aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCy Schubert <cy@FreeBSD.org>2025-04-17 02:13:41 +0000
committerCy Schubert <cy@FreeBSD.org>2025-05-27 16:20:06 +0000
commit24f0b4ca2d565cdbb4fe7839ff28320706bf2386 (patch)
treebc9ce87edb73f767f5580887d0fc8c643b9d7a49 /tests
Diffstat (limited to 'tests')
-rw-r--r--tests/README252
-rw-r--r--tests/TESTS46
-rw-r--r--tests/config/README70
-rw-r--r--tests/data/cppcheck.supp72
-rwxr-xr-xtests/data/generate-krb5-conf86
-rw-r--r--tests/data/krb5-pam.conf30
-rw-r--r--tests/data/krb5.conf30
-rw-r--r--tests/data/perl.conf19
-rw-r--r--tests/data/scripts/alt-auth/basic19
-rw-r--r--tests/data/scripts/alt-auth/basic-debug25
-rw-r--r--tests/data/scripts/alt-auth/fail19
-rw-r--r--tests/data/scripts/alt-auth/fail-debug28
-rw-r--r--tests/data/scripts/alt-auth/fallback25
-rw-r--r--tests/data/scripts/alt-auth/fallback-debug38
-rw-r--r--tests/data/scripts/alt-auth/fallback-realm25
-rw-r--r--tests/data/scripts/alt-auth/force19
-rw-r--r--tests/data/scripts/alt-auth/force-fail-debug26
-rw-r--r--tests/data/scripts/alt-auth/force-fallback25
-rw-r--r--tests/data/scripts/alt-auth/only19
-rw-r--r--tests/data/scripts/alt-auth/only-fail22
-rw-r--r--tests/data/scripts/alt-auth/username-map19
-rw-r--r--tests/data/scripts/alt-auth/username-map-prefix19
-rw-r--r--tests/data/scripts/bad-authtok/no-prompt25
-rw-r--r--tests/data/scripts/bad-authtok/try-first25
-rw-r--r--tests/data/scripts/bad-authtok/try-first-debug36
-rw-r--r--tests/data/scripts/bad-authtok/use-first22
-rw-r--r--tests/data/scripts/bad-authtok/use-first-debug33
-rw-r--r--tests/data/scripts/basic/force-first22
-rw-r--r--tests/data/scripts/basic/force-first-debug32
-rw-r--r--tests/data/scripts/basic/ignore-root16
-rw-r--r--tests/data/scripts/basic/ignore-root-debug24
-rw-r--r--tests/data/scripts/basic/minimum-uid13
-rw-r--r--tests/data/scripts/basic/minimum-uid-debug21
-rw-r--r--tests/data/scripts/basic/no-context17
-rw-r--r--tests/data/scripts/basic/no-context-debug47
-rw-r--r--tests/data/scripts/cache-cleanup/auth-only17
-rw-r--r--tests/data/scripts/cache/basic21
-rw-r--r--tests/data/scripts/cache/end-data-silent27
-rw-r--r--tests/data/scripts/cache/open-session20
-rw-r--r--tests/data/scripts/cache/search-k5login20
-rw-r--r--tests/data/scripts/cache/search-k5login-debug34
-rw-r--r--tests/data/scripts/expired/basic-heimdal31
-rw-r--r--tests/data/scripts/expired/basic-heimdal-debug44
-rw-r--r--tests/data/scripts/expired/basic-heimdal-flag-silent27
-rw-r--r--tests/data/scripts/expired/basic-heimdal-old30
-rw-r--r--tests/data/scripts/expired/basic-heimdal-old-debug43
-rw-r--r--tests/data/scripts/expired/basic-heimdal-silent27
-rw-r--r--tests/data/scripts/expired/basic-mit28
-rw-r--r--tests/data/scripts/expired/basic-mit-debug41
-rw-r--r--tests/data/scripts/expired/basic-mit-flag-silent27
-rw-r--r--tests/data/scripts/expired/basic-mit-silent27
-rw-r--r--tests/data/scripts/expired/defer-mit33
-rw-r--r--tests/data/scripts/expired/defer-mit-debug57
-rw-r--r--tests/data/scripts/expired/fail20
-rw-r--r--tests/data/scripts/expired/fail-debug24
-rw-r--r--tests/data/scripts/fast/anonymous17
-rw-r--r--tests/data/scripts/fast/anonymous-debug22
-rw-r--r--tests/data/scripts/fast/ccache17
-rw-r--r--tests/data/scripts/fast/ccache-debug21
-rw-r--r--tests/data/scripts/fast/no-ccache17
-rw-r--r--tests/data/scripts/fast/no-ccache-debug21
-rw-r--r--tests/data/scripts/long/password14
-rw-r--r--tests/data/scripts/long/password-debug20
-rw-r--r--tests/data/scripts/long/use-first14
-rw-r--r--tests/data/scripts/long/use-first-debug17
-rw-r--r--tests/data/scripts/no-cache/no-prompt25
-rw-r--r--tests/data/scripts/no-cache/no-prompt-try25
-rw-r--r--tests/data/scripts/no-cache/no-prompt-use25
-rw-r--r--tests/data/scripts/no-cache/prompt25
-rw-r--r--tests/data/scripts/no-cache/prompt-expose25
-rw-r--r--tests/data/scripts/no-cache/prompt-fail25
-rw-r--r--tests/data/scripts/no-cache/prompt-fail-debug36
-rw-r--r--tests/data/scripts/no-cache/prompt-principal26
-rw-r--r--tests/data/scripts/no-cache/try-first25
-rw-r--r--tests/data/scripts/no-cache/use-first25
-rw-r--r--tests/data/scripts/pam-user/no-update20
-rw-r--r--tests/data/scripts/pam-user/update20
-rw-r--r--tests/data/scripts/password/authtok21
-rw-r--r--tests/data/scripts/password/authtok-force18
-rw-r--r--tests/data/scripts/password/authtok-too-long17
-rw-r--r--tests/data/scripts/password/authtok-too-long-debug23
-rw-r--r--tests/data/scripts/password/banner23
-rw-r--r--tests/data/scripts/password/banner-expose23
-rw-r--r--tests/data/scripts/password/basic20
-rw-r--r--tests/data/scripts/password/basic-debug28
-rw-r--r--tests/data/scripts/password/expose23
-rw-r--r--tests/data/scripts/password/ignore18
-rw-r--r--tests/data/scripts/password/no-banner23
-rw-r--r--tests/data/scripts/password/no-banner-expose23
-rw-r--r--tests/data/scripts/password/prompt-principal24
-rw-r--r--tests/data/scripts/password/too-long15
-rw-r--r--tests/data/scripts/password/too-long-debug24
-rw-r--r--tests/data/scripts/pkinit/basic22
-rw-r--r--tests/data/scripts/pkinit/basic-debug30
-rw-r--r--tests/data/scripts/pkinit/no-use-pkinit18
-rw-r--r--tests/data/scripts/pkinit/pin-mit20
-rw-r--r--tests/data/scripts/pkinit/preauth-opt-mit17
-rw-r--r--tests/data/scripts/pkinit/prompt-try20
-rw-r--r--tests/data/scripts/pkinit/prompt-use20
-rw-r--r--tests/data/scripts/pkinit/try-pkinit17
-rw-r--r--tests/data/scripts/pkinit/try-pkinit-debug19
-rw-r--r--tests/data/scripts/pkinit/try-pkinit-debug-mit20
-rw-r--r--tests/data/scripts/realm/fail-bad-user-realm17
-rw-r--r--tests/data/scripts/realm/fail-no-realm17
-rw-r--r--tests/data/scripts/realm/fail-no-realm-debug21
-rw-r--r--tests/data/scripts/realm/fail-realm17
-rw-r--r--tests/data/scripts/realm/fail-user-realm18
-rw-r--r--tests/data/scripts/realm/pass-realm17
-rw-r--r--tests/data/scripts/realm/pass-user-realm17
-rw-r--r--tests/data/scripts/stacked/auth-only18
-rw-r--r--tests/data/scripts/stacked/basic22
-rw-r--r--tests/data/scripts/stacked/prompt25
-rw-r--r--tests/data/scripts/stacked/prompt-principal25
-rw-r--r--tests/data/scripts/stacked/try-first22
-rw-r--r--tests/data/scripts/stacked/use-first22
-rw-r--r--tests/data/scripts/trace/supported58
-rw-r--r--tests/data/scripts/trace/unsupported52
-rw-r--r--tests/data/valgrind.supp242
-rwxr-xr-xtests/docs/pod-spelling-t55
-rwxr-xr-xtests/docs/pod-t56
-rwxr-xr-xtests/docs/spdx-license-t149
-rw-r--r--tests/fakepam/README276
-rw-r--r--tests/fakepam/config.c766
-rw-r--r--tests/fakepam/data.c356
-rw-r--r--tests/fakepam/general.c151
-rw-r--r--tests/fakepam/internal.h119
-rw-r--r--tests/fakepam/kuserok.c119
-rw-r--r--tests/fakepam/logging.c183
-rw-r--r--tests/fakepam/pam.h101
-rw-r--r--tests/fakepam/script.c411
-rw-r--r--tests/fakepam/script.h82
-rw-r--r--tests/module/alt-auth-t.c117
-rw-r--r--tests/module/bad-authtok-t.c53
-rw-r--r--tests/module/basic-t.c67
-rw-r--r--tests/module/cache-cleanup-t.c104
-rw-r--r--tests/module/cache-t.c210
-rw-r--r--tests/module/expired-t.c175
-rw-r--r--tests/module/fast-anon-t.c108
-rw-r--r--tests/module/fast-t.c57
-rw-r--r--tests/module/long-t.c46
-rw-r--r--tests/module/no-cache-t.c47
-rw-r--r--tests/module/pam-user-t.c80
-rw-r--r--tests/module/password-t.c152
-rw-r--r--tests/module/pkinit-t.c98
-rw-r--r--tests/module/realm-t.c87
-rw-r--r--tests/module/stacked-t.c50
-rw-r--r--tests/module/trace-t.c48
-rw-r--r--tests/pam-util/args-t.c86
-rw-r--r--tests/pam-util/fakepam-t.c121
-rw-r--r--tests/pam-util/logging-t.c146
-rw-r--r--tests/pam-util/options-t.c458
-rw-r--r--tests/pam-util/vector-t.c149
-rw-r--r--tests/portable/asprintf-t.c69
-rw-r--r--tests/portable/asprintf.c2
-rw-r--r--tests/portable/mkstemp-t.c81
-rw-r--r--tests/portable/mkstemp.c2
-rw-r--r--tests/portable/strndup-t.c60
-rw-r--r--tests/portable/strndup.c2
-rw-r--r--tests/runtests.c1782
-rwxr-xr-xtests/style/obsolete-strings-t104
-rw-r--r--tests/tap/basic.c1029
-rw-r--r--tests/tap/basic.h192
-rw-r--r--tests/tap/kadmin.c138
-rw-r--r--tests/tap/kadmin.h58
-rw-r--r--tests/tap/kerberos.c544
-rw-r--r--tests/tap/kerberos.h135
-rw-r--r--tests/tap/libtap.sh248
-rw-r--r--tests/tap/macros.h99
-rw-r--r--tests/tap/perl/Test/RRA.pm324
-rw-r--r--tests/tap/perl/Test/RRA/Automake.pm487
-rw-r--r--tests/tap/perl/Test/RRA/Config.pm224
-rw-r--r--tests/tap/process.c532
-rw-r--r--tests/tap/process.h95
-rw-r--r--tests/tap/string.c67
-rw-r--r--tests/tap/string.h51
-rwxr-xr-xtests/valgrind/logs-t83
176 files changed, 15173 insertions, 0 deletions
diff --git a/tests/README b/tests/README
new file mode 100644
index 000000000000..186d2d5699b1
--- /dev/null
+++ b/tests/README
@@ -0,0 +1,252 @@
+ Writing TAP Tests
+
+Introduction
+
+ This is a guide for users of the C TAP Harness package or similar
+ TAP-based test harnesses explaining how to write tests. If your
+ package uses C TAP Harness as the test suite driver, you may want to
+ copy this document to an appropriate file name in your test suite as
+ documentation for contributors.
+
+About TAP
+
+ TAP is the Test Anything Protocol, a protocol for communication
+ between test cases and a test harness. This is the protocol used by
+ Perl for its internal test suite and for nearly all Perl modules,
+ since it's the format used by the build tools for Perl modules to run
+ tests and report their results.
+
+ A TAP-based test suite works with a somewhat different set of
+ assumptions than an xUnit test suite. In TAP, each test case is a
+ separate program. That program, when run, must produce output in the
+ following format:
+
+ 1..4
+ ok 1 - the first test
+ ok 2
+ # a diagnostic, ignored by the harness
+ not ok 3 - a failing test
+ ok 4 # skip a skipped test
+
+ The output should all go to standard output. The first line specifies
+ the number of tests to be run, and then each test produces output that
+ looks like either "ok <n>" or "not ok <n>" depending on whether the
+ test succeeded or failed. Additional information about the test can
+ be provided after the "ok <n>" or "not ok <n>", but is optional.
+ Additional diagnostics and information can be provided in lines
+ beginning with a "#".
+
+ Processing directives are supported after the "ok <n>" or "not ok <n>"
+ and start with a "#". The main one of interest is "# skip" which says
+ that the test was skipped rather than successful and optionally gives
+ the reason. Also supported is "# todo", which normally annotates a
+ failing test and indicates that test is expected to fail, optionally
+ providing a reason for why.
+
+ There are three more special cases. First, the initial line stating
+ the number of tests to run, called the plan, may appear at the end of
+ the output instead of the beginning. This can be useful if the number
+ of tests to run is not known in advance. Second, a plan in the form:
+
+ 1..0 # skip entire test case skipped
+
+ can be given instead, which indicates that this entire test case has
+ been skipped (generally because it depends on facilities or optional
+ configuration which is not present). Finally, if the test case
+ encounters a fatal error, it should print the text:
+
+ Bail out!
+
+ on standard output, optionally followed by an error message, and then
+ exit. This tells the harness that the test aborted unexpectedly.
+
+ The exit status of a successful test case should always be 0. The
+ harness will report the test as "dubious" if all the tests appeared to
+ succeed but it exited with a non-zero status.
+
+Writing TAP Tests
+
+ Environment
+
+ One of the special features of C TAP Harness is the environment that
+ it sets up for your test cases. If your test program is called under
+ the runtests driver, the environment variables C_TAP_SOURCE and
+ C_TAP_BUILD will be set to the top of the test directory in the source
+ tree and the top of the build tree, respectively. You can use those
+ environment variables to locate additional test data, programs and
+ libraries built as part of your software build, and other supporting
+ information needed by tests.
+
+ The C and shell TAP libraries support a test_file_path() function,
+ which looks for a file under the build tree and then under the source
+ tree, using the C_TAP_BUILD and C_TAP_SOURCE environment variables,
+ and return the full path to the file. This can be used to locate
+ supporting data files. They also support a test_tmpdir() function
+ that returns a directory that can be used for temporary files during
+ tests.
+
+ Perl
+
+ Since TAP is the native test framework for Perl, writing TAP tests in
+ Perl is very easy and extremely well-supported. If you've never
+ written tests in Perl before, start by reading the documentation for
+ Test::Tutorial and Test::Simple, which walks you through the basics,
+ including the TAP output syntax. Then, the best Perl module to use
+ for serious testing is Test::More, which provides a lot of additional
+ functions over Test::Simple including support for skipping tests,
+ bailing out, and not planning tests in advance. See the documentation
+ of Test::More for all the details and lots of examples.
+
+ C TAP Harness can run Perl test scripts directly and interpret the
+ results correctly, and similarly the Perl Test::Harness module and
+ prove command can run TAP tests written in other languages using, for
+ example, the TAP library that comes with C TAP Harness. You can, if
+ you wish, use the library that comes with C TAP Harness but use prove
+ instead of runtests for running the test suite.
+
+ C
+
+ C TAP Harness provides a basic TAP library that takes away most of the
+ pain of writing TAP test cases in C. A C test case should start with
+ a call to plan(), passing in the number of tests to run. Then, each
+ test should use is_int(), is_string(), is_double(), or is_hex() as
+ appropriate to compare expected and seen values, or ok() to do a
+ simpler boolean test. The is_*() functions take expected and seen
+ values and then a printf-style format string explaining the test
+ (which may be NULL). ok() takes a boolean and then the printf-style
+ string.
+
+ Here's a complete example test program that uses the C TAP library:
+
+ #include <stddef.h>
+ #include <tap/basic.h>
+
+ int
+ main(void)
+ {
+ plan(4);
+
+ ok(1, "the first test");
+ is_int(42, 42, NULL);
+ diag("a diagnostic, ignored by the harness");
+ ok(0, "a failing test");
+ skip("a skipped test");
+
+ return 0;
+ }
+
+ This test program produces the output shown above in the section on
+ TAP and demonstrates most of the functions. The other functions of
+ interest are sysdiag() (like diag() but adds strerror() results),
+ bail() and sysbail() for fatal errors, skip_block() to skip a whole
+ block of tests, and skip_all() which is called instead of plan() to
+ skip an entire test case.
+
+ The C TAP library also provides plan_lazy(), which can be called
+ instead of plan(). If plan_lazy() is called, the library will keep
+ track of how many test results are reported and will print out the
+ plan at the end of execution of the program. This should normally be
+ avoided since the test may appear to be successful even if it exits
+ prematurely, but it can make writing tests easier in some
+ circumstances.
+
+ Complete API documentation for the basic C TAP library that comes with
+ C TAP Harness is available at:
+
+ <https://www.eyrie.org/~eagle/software/c-tap-harness/>
+
+ It's common to need additional test functions and utility functions
+ for your C tests, particularly if you have to set up and tear down a
+ test environment for your test programs, and it's useful to have them
+ all in the libtap library so that you only have to link your test
+ programs with one library. Rather than editing tap/basic.c and
+ tap/basic.h to add those additional functions, add additional *.c and
+ *.h files into the tap directory with the function implementations and
+ prototypes, and then add those additional objects to the library.
+ That way, you can update tap/basic.c and tap/basic.h from subsequent
+ releases of C TAP Harness without having to merge changes with your
+ own code.
+
+ Libraries of additional useful TAP test functions are available in
+ rra-c-util at:
+
+ <https://www.eyrie.org/~eagle/software/rra-c-util/>
+
+ Some of the code there is particularly useful when testing programs
+ that require Kerberos keys.
+
+ If you implement new test functions that compare an expected and seen
+ value, it's best to name them is_<something> and take the expected
+ value, the seen value, and then a printf-style format string and
+ possible arguments to match the calling convention of the functions
+ provided by C TAP Harness.
+
+ Shell
+
+ C TAP Harness provides a library of shell functions to make it easier
+ to write TAP tests in shell. That library includes much of the same
+ functionality as the C TAP library, but takes its parameters in a
+ somewhat different order to make better use of shell features.
+
+ The libtap.sh file should be installed in a directory named tap in
+ your test suite area. It can then be loaded by tests written in shell
+ using the environment set up by runtests with:
+
+ . "$C_TAP_SOURCE"/tap/libtap.sh
+
+ Here is a complete test case written in shell which produces the same
+ output as the TAP sample above:
+
+ #!/bin/sh
+
+ . "$C_TAP_SOURCE"/tap/libtap.sh
+ cd "$C_TAP_BUILD"
+
+ plan 4
+ ok 'the first test' true
+ ok '' [ 42 -eq 42 ]
+ diag a diagnostic, ignored by the harness
+ ok '' false
+ skip 'a skipped test'
+
+ The shell framework doesn't provide the is_* functions, so you'll use
+ the ok function more. It takes a string describing the text and then
+ treats all of its remaining arguments as a condition, evaluated the
+ same way as the arguments to the "if" statement. If that condition
+ evaluates to true, the test passes; otherwise, the test fails.
+
+ The plan, plan_lazy, diag, and bail functions work the same as with
+ the C library. skip takes a string and skips the next test with that
+ explanation. skip_block takes a count and a string and skips that
+ many tests with that explanation. skip_all takes an optional reason
+ and skips the entire test case.
+
+ Since it's common for shell programs to want to test the output of
+ commands, there's an additional function ok_program provided by the
+ shell test library. It takes the test description string, the
+ expected exit status, the expected program output, and then treats the
+ rest of its arguments as the program to run. That program is run with
+ standard error and standard output combined, and then its exit status
+ and output are tested against the provided values.
+
+ A utility function, strip_colon_error, is provided that runs the
+ command given as its arguments and strips text following a colon and a
+ space from the output (unless there is no whitespace on the line
+ before the colon and the space, normally indicating a prefix of the
+ program name). This function can be used to wrap commands that are
+ expected to fail with output that has a system- or locale-specific
+ error message appended, such as the output of strerror().
+
+License
+
+ This file is part of the documentation of C TAP Harness, which can be
+ found at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+
+ Copyright 2010, 2016 Russ Allbery <eagle@eyrie.org>
+
+ Copying and distribution of this file, with or without modification,
+ are permitted in any medium without royalty provided the copyright
+ notice and this notice are preserved. This file is offered as-is,
+ without any warranty.
+
+ SPDX-License-Identifier: FSFAP
diff --git a/tests/TESTS b/tests/TESTS
new file mode 100644
index 000000000000..f9036b1569c4
--- /dev/null
+++ b/tests/TESTS
@@ -0,0 +1,46 @@
+# Test list for pam-krb5. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2011-2012
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice and
+# this notice are preserved. This file is offered as-is, without any
+# warranty.
+#
+# SPDX-License-Identifier: FSFAP
+
+# Exclude the tests that use the pkinit.so MIT Kerberos module from valgrind
+# testing (module/fast-anon and module/pkinit) because they cause valgrind to
+# go into an infinite loop.
+
+docs/pod
+docs/pod-spelling
+docs/spdx-license
+module/alt-auth valgrind
+module/bad-authtok valgrind
+module/basic valgrind
+module/cache valgrind
+module/cache-cleanup valgrind
+module/expired valgrind
+module/fast valgrind
+module/fast-anon
+module/long valgrind
+module/no-cache valgrind
+module/pam-user valgrind
+module/password valgrind
+module/pkinit
+module/realm valgrind
+module/stacked valgrind
+pam-util/args valgrind
+pam-util/fakepam valgrind
+pam-util/logging valgrind
+pam-util/options valgrind
+pam-util/vector valgrind
+portable/asprintf valgrind
+portable/mkstemp valgrind
+portable/strndup valgrind
+style/obsolete-strings
+valgrind/logs
diff --git a/tests/config/README b/tests/config/README
new file mode 100644
index 000000000000..a034b35b6b0d
--- /dev/null
+++ b/tests/config/README
@@ -0,0 +1,70 @@
+This directory contains configuration required to run the complete
+pam-krb5 test suite. If there is no configuration in this directory, many
+of the tests will be skipped. To enable the full test suite, create the
+following files:
+
+admin-keytab
+
+ A keytab for a principal (in the same realm as the test principal
+ configured in password) that has admin access to inspect and modify
+ that test principal. For an MIT Kerberos KDC, it needs "mci"
+ permissions in kadm5.acl for that principal. For a Heimdal KDC, it
+ needs "cpw,list,modify" permissions (obviously, "all" will do). This
+ file is optional; if not present, the tests requiring admin
+ modification of a principal will be skipped.
+
+krb5.conf
+
+ This is optional and not required if the Kerberos realm used for
+ testing is configured in DNS or your system krb5.conf file and that
+ file is in either /etc/krb5.conf or /usr/local/etc/krb5.conf.
+ Otherwise, create a krb5.conf file that contains the realm information
+ (KDC, kpasswd server, and admin server) for the realm you're using for
+ testing. You don't need to worry about setting the default realm;
+ this will be done automatically in the generated file used by the test
+ suite.
+
+keytab
+
+ An optional keytab for a principal, which generally should be in the
+ same realm as the user configured in the password file. This is used
+ to test FAST support with a ticket cache.
+
+password
+
+ This file should contain two lines. The first line is the
+ fully-qualified principal (including the realm) of a Kerberos
+ principal to use for testing authentication. The second line is the
+ password for that principal.
+
+ If the realm of the principal is not configured in either DNS or in
+ your system krb5.conf file (/usr/local/etc/krb5.conf or
+ /etc/krb5.conf) with the KDC, kpasswd server, and admin server, you
+ will need to also provide a krb5.conf file in this directory. See
+ below.
+
+pkinit-cert
+
+ Certificate and private key (concatenated together) for PKINIT
+ authentication for the user listed in the pkinit-principal file.
+ Optional; PKINIT checks will be skipped if this file isn't present.
+
+pkinit-principal
+
+ Principal to use to test PKINIT authentication. Must be the Kerberos
+ identity corresponding to the certificate and private key given in
+ pkinit-cert. Optional; PKINIT checks will be skipped if this file
+ isn't present.
+
+-----
+
+Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+Copyright 2011-2012
+ The Board of Trustees of the Leland Stanford Junior University
+
+Copying and distribution of this file, with or without modification, are
+permitted in any medium without royalty provided the copyright notice and
+this notice are preserved. This file is offered as-is, without any
+warranty.
+
+SPDX-License-Identifier: FSFAP
diff --git a/tests/data/cppcheck.supp b/tests/data/cppcheck.supp
new file mode 100644
index 000000000000..00734778b256
--- /dev/null
+++ b/tests/data/cppcheck.supp
@@ -0,0 +1,72 @@
+// Suppressions file for cppcheck. -*- conf -*-
+//
+// This includes suppressions for all of my projects, including files that
+// aren't in rra-c-util, for ease of sharing between projects. The ones that
+// don't apply to a particular project should hopefully be harmless.
+//
+// To determine the correct suppression to add for a new error, run cppcheck
+// with the --xml flag and then add a suppression for the error id, file
+// location, and line.
+//
+// Copyright 2018-2021 Russ Allbery <eagle@eyrie.org>
+//
+// Copying and distribution of this file, with or without modification, are
+// permitted in any medium without royalty provided the copyright notice and
+// this notice are preserved. This file is offered as-is, without any
+// warranty.
+//
+// SPDX-License-Identifier: FSFAP
+
+// I like declaring variables at the top of a function rather than cluttering
+// every if and loop body with declarations.
+variableScope
+
+// strlen of a constant string is more maintainable code than hard-coding the
+// string length.
+constArgument:tests/runtests.c:804
+
+// False positive due to recursive function.
+knownConditionTrueFalse:portable/getopt.c:146
+
+// Bug in cppcheck 2.3. cppcheck can't see the assignment because of the
+// void * cast.
+knownConditionTrueFalse:portable/k_haspag.c:61
+
+// False positive since the string comes from a command-line define.
+knownConditionTrueFalse:tests/tap/process.c:415
+knownConditionTrueFalse:tests/tap/remctl.c:79
+
+// Stored in the returned ai struct, but cppcheck can't see the assignment
+// because of the struct sockaddr * cast.
+memleak:portable/getaddrinfo.c:236
+
+// Bug in cppcheck 1.89 (fixed in 2.3). The address of this variable is
+// passed to a Windows function (albeit through a cast).
+nullPointer:portable/winsock.c:61
+
+// Bug in cppcheck 2.3.
+nullPointerRedundantCheck:portable/krb5-profile.c:61
+
+// Bug in cppcheck 2.3.
+nullPointerRedundantCheck:portable/krb5-renew.c:82
+nullPointerRedundantCheck:portable/krb5-renew.c:83
+
+// Setting the variable to NULL explicitly after deallocation.
+redundantAssignment:tests/pam-util/options-t.c
+
+// (remctl) Bug in cppcheck 1.89 (fixed in 2.3). The address of these
+// variables are passed to a PHP function.
+uninitvar:php/php_remctl.c:119
+uninitvar:php/php_remctl.c:123
+uninitvar:php/php_remctl.c:315
+uninitvar:php/php5_remctl.c:125
+uninitvar:php/php5_remctl.c:129
+uninitvar:php/php5_remctl.c:321
+
+// (remctl) Bug in cppcheck 1.82. A pointer to this array is stored in a
+// struct that's passed to another function.
+redundantAssignment:tests/server/acl-t.c
+
+// (pam-krb5) cppcheck doesn't recognize the unused attribute on labels.
+unusedLabel:module/auth.c:895
+unusedLabelConfiguration:module/auth.c:895
diff --git a/tests/data/generate-krb5-conf b/tests/data/generate-krb5-conf
new file mode 100755
index 000000000000..712a933d40ba
--- /dev/null
+++ b/tests/data/generate-krb5-conf
@@ -0,0 +1,86 @@
+#!/bin/sh
+
+# Generate a krb5.conf file in the current directory for testing purposes.
+# Takes one command-line argument: the default realm to use. Strips out the
+# entire [appdefaults] section to avoid picking up any local configuration and
+# sets the default realm as indicated.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2016, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2006-2008, 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+set -e
+
+# Load the test library.
+. "$C_TAP_SOURCE/tap/libtap.sh"
+cd "$C_TAP_BUILD"
+
+# If there is no default realm specified on the command line, we leave the
+# realm information alone.
+realm="$1"
+
+# Locate the krb5.conf file to use as a base. Prefer the one in the test
+# configuration area, if it exists.
+krb5conf=`test_file_path config/krb5.conf`
+if [ -z "$krb5conf" ] ; then
+ for p in /etc/krb5.conf /usr/local/etc/krb5.conf ; do
+ if [ -r "$p" ] ; then
+ krb5conf="$p"
+ break
+ fi
+ done
+fi
+if [ -z "$krb5conf" ] ; then
+ echo 'no krb5.conf found, see test instructions' >&2
+ exit 1
+fi
+
+# We found a krb5.conf file. Generate our munged one.
+mkdir -p tmp
+awk '
+ BEGIN { skip = 0 }
+ /^ *\[appdefaults\]/ { skip = 1 }
+ !/^ *\[appdefaults\]/ && / *\[/ { skip = 0 }
+
+ { if (skip == 0) print }
+' "$krb5conf" > tmp/krb5.conf.tmp
+if [ -n "$realm" ] ; then
+ pattern='^[ ]*default_realm.*='
+ if grep "$pattern" tmp/krb5.conf.tmp >/dev/null 2>/dev/null; then
+ sed -e "s/\\(default_realm.*=\\) .*/\\1 $realm/" \
+ tmp/krb5.conf.tmp >tmp/krb5.conf
+ else
+ (
+ cat tmp/krb5.conf.tmp
+ echo "[libdefaults]"
+ echo " default_realm = $realm"
+ ) >tmp/krb5.conf
+ fi
+ rm tmp/krb5.conf.tmp
+else
+ mv tmp/krb5.conf.tmp tmp/krb5.conf
+fi
diff --git a/tests/data/krb5-pam.conf b/tests/data/krb5-pam.conf
new file mode 100644
index 000000000000..57887882c954
--- /dev/null
+++ b/tests/data/krb5-pam.conf
@@ -0,0 +1,30 @@
+# Test krb5.conf file for PAM option parsing.
+
+[appdefaults]
+ FOO.COM = {
+ program = /bin/false
+ }
+ BAR.COM = {
+ program = echo /bin/true
+ }
+ testing = {
+ minimum_uid = 1000
+ ignore_root = false
+ expires = 30m
+ FOO.COM = {
+ cells = foo.com,bar.com
+ }
+ BAR.COM = {
+ cells = bar.com foo.com
+ }
+ }
+ other-test = {
+ minimum_uid = -1000
+ }
+ bad-number = {
+ minimum_uid = 1000foo
+ }
+ bad-time = {
+ expires = ft87
+ }
+ debug = true
diff --git a/tests/data/krb5.conf b/tests/data/krb5.conf
new file mode 100644
index 000000000000..57887882c954
--- /dev/null
+++ b/tests/data/krb5.conf
@@ -0,0 +1,30 @@
+# Test krb5.conf file for PAM option parsing.
+
+[appdefaults]
+ FOO.COM = {
+ program = /bin/false
+ }
+ BAR.COM = {
+ program = echo /bin/true
+ }
+ testing = {
+ minimum_uid = 1000
+ ignore_root = false
+ expires = 30m
+ FOO.COM = {
+ cells = foo.com,bar.com
+ }
+ BAR.COM = {
+ cells = bar.com foo.com
+ }
+ }
+ other-test = {
+ minimum_uid = -1000
+ }
+ bad-number = {
+ minimum_uid = 1000foo
+ }
+ bad-time = {
+ expires = ft87
+ }
+ debug = true
diff --git a/tests/data/perl.conf b/tests/data/perl.conf
new file mode 100644
index 000000000000..699ef3a9123a
--- /dev/null
+++ b/tests/data/perl.conf
@@ -0,0 +1,19 @@
+# Configuration for Perl tests. -*- perl -*-
+
+# Ignore these top-level directories for perlcritic testing.
+@CRITIC_IGNORE = qw();
+
+# Add this directory (or a .libs subdirectory) relative to the top of the
+# source tree to LD_LIBRARY_PATH when checking the syntax of Perl modules.
+# This may be required to pick up libraries that are used by in-tree Perl
+# modules.
+#$LIBRARY_PATH = 'lib';
+
+# Default minimum version requirement for included Perl scripts.
+$MINIMUM_VERSION = '5.006';
+
+# Minimum version exceptions for specific top-level directories.
+%MINIMUM_VERSION = ();
+
+# File must end with this line.
+1;
diff --git a/tests/data/scripts/alt-auth/basic b/tests/data/scripts/alt-auth/basic
new file mode 100644
index 000000000000..92628e98cd8f
--- /dev/null
+++ b/tests/data/scripts/alt-auth/basic
@@ -0,0 +1,19 @@
+# Test simplest case of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_first_pass no_ccache
+ account = alt_auth_map=%1 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/basic-debug b/tests/data/scripts/alt-auth/basic-debug
new file mode 100644
index 000000000000..325a8117284c
--- /dev/null
+++ b/tests/data/scripts/alt-auth/basic-debug
@@ -0,0 +1,25 @@
+# Test simplest case of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_first_pass no_ccache debug
+ account = alt_auth_map=%1 no_ccache debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping bogus-nonexistent-account to %1
+ DEBUG (user %u) alternate authentication successful
+ INFO user %u authenticated as %1
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG pam_sm_acct_mgmt: exit (success)
diff --git a/tests/data/scripts/alt-auth/fail b/tests/data/scripts/alt-auth/fail
new file mode 100644
index 000000000000..ec2145f3098f
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fail
@@ -0,0 +1,19 @@
+# Test failure of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=bogus force_first_pass no_ccache
+ account = alt_auth_map=bogus no_ccache
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+ acct_mgmt = PAM_IGNORE
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/alt-auth/fail-debug b/tests/data/scripts/alt-auth/fail-debug
new file mode 100644
index 000000000000..ae96bb148e6a
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fail-debug
@@ -0,0 +1,28 @@
+# Test failure of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=bogus force_first_pass no_ccache debug
+ account = alt_auth_map=bogus no_ccache debug
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+ acct_mgmt = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping bogus-nonexistent-account to bogus@%2
+ DEBUG /^\(user %u\) alternate authentication failed: /
+ DEBUG (user %u) attempting authentication as %u@%2
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
diff --git a/tests/data/scripts/alt-auth/fallback b/tests/data/scripts/alt-auth/fallback
new file mode 100644
index 000000000000..a0ee7a3d4292
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fallback
@@ -0,0 +1,25 @@
+# Test alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s/unknown-user no_ccache
+ account = alt_auth_map=%%s/unknown-user no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/alt-auth/fallback-debug b/tests/data/scripts/alt-auth/fallback-debug
new file mode 100644
index 000000000000..f63741a60a16
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fallback-debug
@@ -0,0 +1,38 @@
+# Test alternative authentication principal with debug logging. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s/unknown-user no_ccache debug
+ account = alt_auth_map=%%s/unknown-user no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping %u to %0/unknown-user@%2
+ DEBUG /^\(user %u\) alternate authentication failed: /
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG (user %u) mapped user %0/unknown-user@%2 does not match principal %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) mapped user %0/unknown-user@%2 does not match principal %u
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/alt-auth/fallback-realm b/tests/data/scripts/alt-auth/fallback-realm
new file mode 100644
index 000000000000..0eef10fd5056
--- /dev/null
+++ b/tests/data/scripts/alt-auth/fallback-realm
@@ -0,0 +1,25 @@
+# Test alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s@BOGUS.EXAMPLE.COM no_ccache
+ account = alt_auth_map=%%s@BOGUS.EXAMPLE.COM no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/alt-auth/force b/tests/data/scripts/alt-auth/force
new file mode 100644
index 000000000000..4ad34f6f1fe4
--- /dev/null
+++ b/tests/data/scripts/alt-auth/force
@@ -0,0 +1,19 @@
+# Test forced alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_alt_auth force_first_pass no_ccache
+ account = alt_auth_map=%1 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/force-fail-debug b/tests/data/scripts/alt-auth/force-fail-debug
new file mode 100644
index 000000000000..cc077b1a4743
--- /dev/null
+++ b/tests/data/scripts/alt-auth/force-fail-debug
@@ -0,0 +1,26 @@
+# Test failure of forced authentication principal (no fallback). -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 force_alt_auth force_first_pass no_ccache debug
+ account = alt_auth_map=%1 no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) mapping bogus-nonexistent-account to %1
+ DEBUG /^\(user %u\) alternate authentication failed: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
diff --git a/tests/data/scripts/alt-auth/force-fallback b/tests/data/scripts/alt-auth/force-fallback
new file mode 100644
index 000000000000..b93b04175ed5
--- /dev/null
+++ b/tests/data/scripts/alt-auth/force-fallback
@@ -0,0 +1,25 @@
+# Test forced alternative authentication with fallback. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s/unknown-user force_alt_auth no_ccache
+ account = alt_auth_map=%%s/unknown-user no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/alt-auth/only b/tests/data/scripts/alt-auth/only
new file mode 100644
index 000000000000..7761fc7fd0ce
--- /dev/null
+++ b/tests/data/scripts/alt-auth/only
@@ -0,0 +1,19 @@
+# Test required alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%1 only_alt_auth force_first_pass no_ccache
+ account = alt_auth_map=%1 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/only-fail b/tests/data/scripts/alt-auth/only-fail
new file mode 100644
index 000000000000..5c2831614928
--- /dev/null
+++ b/tests/data/scripts/alt-auth/only-fail
@@ -0,0 +1,22 @@
+# Test failure of required alternative authentication. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=bogus only_alt_auth no_ccache
+ account = alt_auth_map=bogus no_ccache
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ acct_mgmt = PAM_IGNORE
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/alt-auth/username-map b/tests/data/scripts/alt-auth/username-map
new file mode 100644
index 000000000000..7f28a670344b
--- /dev/null
+++ b/tests/data/scripts/alt-auth/username-map
@@ -0,0 +1,19 @@
+# Test username mapping of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%%s@%2 force_first_pass no_ccache
+ account = alt_auth_map=%%s@%2 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/alt-auth/username-map-prefix b/tests/data/scripts/alt-auth/username-map-prefix
new file mode 100644
index 000000000000..5e83fc888d77
--- /dev/null
+++ b/tests/data/scripts/alt-auth/username-map-prefix
@@ -0,0 +1,19 @@
+# Test username mapping of alternative authentication principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = alt_auth_map=%3%%s@%2 force_first_pass no_ccache
+ account = alt_auth_map=%3%%s@%2 no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %1
diff --git a/tests/data/scripts/bad-authtok/no-prompt b/tests/data/scripts/bad-authtok/no-prompt
new file mode 100644
index 000000000000..e0c10cc69804
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/no-prompt
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library after bad authtok. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt try_first_pass
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/bad-authtok/try-first b/tests/data/scripts/bad-authtok/try-first
new file mode 100644
index 000000000000..cde6153efaeb
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/try-first
@@ -0,0 +1,25 @@
+# Test try_first_pass with a bad initial AUTHTOK. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/bad-authtok/try-first-debug b/tests/data/scripts/bad-authtok/try-first-debug
new file mode 100644
index 000000000000..c76ce7ac89dd
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/try-first-debug
@@ -0,0 +1,36 @@
+# Test try_first_pass with a bad initial AUTHTOK and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/bad-authtok/use-first b/tests/data/scripts/bad-authtok/use-first
new file mode 100644
index 000000000000..62d55ca2146f
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/use-first
@@ -0,0 +1,22 @@
+# Test use_first_pass with a bad initial AUTHTOK. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/bad-authtok/use-first-debug b/tests/data/scripts/bad-authtok/use-first-debug
new file mode 100644
index 000000000000..4346d2395cb0
--- /dev/null
+++ b/tests/data/scripts/bad-authtok/use-first-debug
@@ -0,0 +1,33 @@
+# Test use_first_pass with a bad initial AUTHTOK and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/basic/force-first b/tests/data/scripts/basic/force-first
new file mode 100644
index 000000000000..792d737ba7c3
--- /dev/null
+++ b/tests/data/scripts/basic/force-first
@@ -0,0 +1,22 @@
+# Test force_first_pass without an authtok. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/basic/force-first-debug b/tests/data/scripts/basic/force-first-debug
new file mode 100644
index 000000000000..539345316183
--- /dev/null
+++ b/tests/data/scripts/basic/force-first-debug
@@ -0,0 +1,32 @@
+# Test force_first_pass without an authtok. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) no stored password
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/basic/ignore-root b/tests/data/scripts/basic/ignore-root
new file mode 100644
index 000000000000..bfbfee5c86df
--- /dev/null
+++ b/tests/data/scripts/basic/ignore-root
@@ -0,0 +1,16 @@
+# Test account and session behavior for ignored root user. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_root
+ password = ignore_root
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
diff --git a/tests/data/scripts/basic/ignore-root-debug b/tests/data/scripts/basic/ignore-root-debug
new file mode 100644
index 000000000000..2ffd33c16229
--- /dev/null
+++ b/tests/data/scripts/basic/ignore-root-debug
@@ -0,0 +1,24 @@
+# Test account and session behavior for ignored root user. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_root debug
+ password = ignore_root debug
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user root) ignoring root user
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG ignoring root user
+ DEBUG pam_sm_chauthtok: exit (ignore)
diff --git a/tests/data/scripts/basic/minimum-uid b/tests/data/scripts/basic/minimum-uid
new file mode 100644
index 000000000000..e56161041306
--- /dev/null
+++ b/tests/data/scripts/basic/minimum-uid
@@ -0,0 +1,13 @@
+# Test account and session behavior for minimum UID. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = minimum_uid=%1
+ password = minimum_uid=%1
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
diff --git a/tests/data/scripts/basic/minimum-uid-debug b/tests/data/scripts/basic/minimum-uid-debug
new file mode 100644
index 000000000000..c20e43d55ac8
--- /dev/null
+++ b/tests/data/scripts/basic/minimum-uid-debug
@@ -0,0 +1,21 @@
+# Test account and session behavior for minimum UID (debug). -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = minimum_uid=%1 debug
+ password = minimum_uid=%1 debug
+
+[run]
+ authenticate = PAM_USER_UNKNOWN
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) ignoring low-UID user (%0 < %1)
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG ignoring low-UID user (%0 < %1)
+ DEBUG pam_sm_chauthtok: exit (ignore)
diff --git a/tests/data/scripts/basic/no-context b/tests/data/scripts/basic/no-context
new file mode 100644
index 000000000000..5629422e23d9
--- /dev/null
+++ b/tests/data/scripts/basic/no-context
@@ -0,0 +1,17 @@
+# Test account and session behavior with no context. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_IGNORE
+ close_session = PAM_SUCCESS
diff --git a/tests/data/scripts/basic/no-context-debug b/tests/data/scripts/basic/no-context-debug
new file mode 100644
index 000000000000..4bdeee727ed7
--- /dev/null
+++ b/tests/data/scripts/basic/no-context-debug
@@ -0,0 +1,47 @@
+# Test account and session behavior with no context. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug
+ account = debug
+ session = debug
+
+[run]
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_IGNORE
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_setcred: entry (delete)
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (establish)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (refresh)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_setcred: entry (reinit)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_open_session: exit (ignore)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/cache-cleanup/auth-only b/tests/data/scripts/cache-cleanup/auth-only
new file mode 100644
index 000000000000..c29608f3c8da
--- /dev/null
+++ b/tests/data/scripts/cache-cleanup/auth-only
@@ -0,0 +1,17 @@
+# Test authentication only with ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login ccache_dir=FILE:%1
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/basic b/tests/data/scripts/cache/basic
new file mode 100644
index 000000000000..6b1042f3084b
--- /dev/null
+++ b/tests/data/scripts/cache/basic
@@ -0,0 +1,21 @@
+# Test basic authentication with ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login
+ account = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/end-data-silent b/tests/data/scripts/cache/end-data-silent
new file mode 100644
index 000000000000..f172008bc574
--- /dev/null
+++ b/tests/data/scripts/cache/end-data-silent
@@ -0,0 +1,27 @@
+# Test pam_end with PAM_DATA_SILENT. -*- conf -*-
+#
+# Passing PAM_DATA_SILENT to pam_end should cause the credential cache to not
+# be deleted (under the assumption that pam_end is being called in a forked
+# process and will be called again in the parent to clean up resources).
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020-2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login
+ account = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+
+[end]
+ flags = PAM_DATA_SILENT
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/open-session b/tests/data/scripts/cache/open-session
new file mode 100644
index 000000000000..83e48c36511e
--- /dev/null
+++ b/tests/data/scripts/cache/open-session
@@ -0,0 +1,20 @@
+# Test authentication with ticket cache, open session. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass ignore_k5login
+ account = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/search-k5login b/tests/data/scripts/cache/search-k5login
new file mode 100644
index 000000000000..b87c28147edb
--- /dev/null
+++ b/tests/data/scripts/cache/search-k5login
@@ -0,0 +1,20 @@
+# Test authentication with search_k5login, open session. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass search_k5login
+ account = search_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/cache/search-k5login-debug b/tests/data/scripts/cache/search-k5login-debug
new file mode 100644
index 000000000000..eb50b9e47eaf
--- /dev/null
+++ b/tests/data/scripts/cache/search-k5login-debug
@@ -0,0 +1,34 @@
+# Test authentication with search_k5login and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass search_k5login debug
+ account = search_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-heimdal b/tests/data/scripts/expired/basic-heimdal
new file mode 100644
index 000000000000..2b4f471cf247
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal
@@ -0,0 +1,31 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password has expired
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-heimdal-debug b/tests/data/scripts/expired/basic-heimdal-debug
new file mode 100644
index 000000000000..a18cc00c71a9
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-debug
@@ -0,0 +1,44 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2017, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login debug
+ account = ignore_k5login debug
+ password = ignore_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password has expired
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-heimdal-flag-silent b/tests/data/scripts/expired/basic-heimdal-flag-silent
new file mode 100644
index 000000000000..58e065b485bb
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-flag-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with PAM_SILENT. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate(SILENT) = PAM_SUCCESS
+ acct_mgmt(SILENT) = PAM_SUCCESS
+ open_session(SILENT) = PAM_SUCCESS
+ close_session(SILENT) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-heimdal-old b/tests/data/scripts/expired/basic-heimdal-old
new file mode 100644
index 000000000000..dd67ec44df7c
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-old
@@ -0,0 +1,30 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-heimdal-old-debug b/tests/data/scripts/expired/basic-heimdal-old-debug
new file mode 100644
index 000000000000..53267f5fac62
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-old-debug
@@ -0,0 +1,43 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login debug
+ account = ignore_k5login debug
+ password = ignore_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Your password will expire at %1
+ info = Changing password
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+ info = Success: Password changed
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-heimdal-silent b/tests/data/scripts/expired/basic-heimdal-silent
new file mode 100644
index 000000000000..028d5fe382f6
--- /dev/null
+++ b/tests/data/scripts/expired/basic-heimdal-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with silent. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login silent
+ account = ignore_k5login silent
+ password = ignore_k5login silent
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = New password: |%n
+ echo_off = Repeat new password: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-mit b/tests/data/scripts/expired/basic-mit
new file mode 100644
index 000000000000..9611381b4ce9
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit
@@ -0,0 +1,28 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password expired. You must change it now.
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-mit-debug b/tests/data/scripts/expired/basic-mit-debug
new file mode 100644
index 000000000000..5b58b25b8ec2
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit-debug
@@ -0,0 +1,41 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login debug
+ account = ignore_k5login debug
+ password = ignore_k5login debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ info = Password expired. You must change it now.
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/basic-mit-flag-silent b/tests/data/scripts/expired/basic-mit-flag-silent
new file mode 100644
index 000000000000..a13bffdeea44
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit-flag-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with PAM_SILENT. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login
+ account = ignore_k5login
+ password = ignore_k5login
+
+[run]
+ authenticate(SILENT) = PAM_SUCCESS
+ acct_mgmt(SILENT) = PAM_SUCCESS
+ open_session(SILENT) = PAM_SUCCESS
+ close_session(SILENT) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/basic-mit-silent b/tests/data/scripts/expired/basic-mit-silent
new file mode 100644
index 000000000000..7dea2b7bdd4e
--- /dev/null
+++ b/tests/data/scripts/expired/basic-mit-silent
@@ -0,0 +1,27 @@
+# Test default handling of expired passwords with silent. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login silent
+ account = ignore_k5login silent
+ password = ignore_k5login silent
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+ echo_off = Enter new password: |%n
+ echo_off = Enter it again: |%n
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/defer-mit b/tests/data/scripts/expired/defer-mit
new file mode 100644
index 000000000000..7403edbfdbbf
--- /dev/null
+++ b/tests/data/scripts/expired/defer-mit
@@ -0,0 +1,33 @@
+# Test deferring handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = defer_pwchange use_first_pass
+ account = ignore_k5login
+ password = ignore_k5login use_first_pass
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_NEW_AUTHTOK_REQD
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ INFO user %u authenticated as %0 (expired)
+ INFO user %u account password is expired
+ INFO user %u changed Kerberos password
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/expired/defer-mit-debug b/tests/data/scripts/expired/defer-mit-debug
new file mode 100644
index 000000000000..c637f39402f7
--- /dev/null
+++ b/tests/data/scripts/expired/defer-mit-debug
@@ -0,0 +1,57 @@
+# Test deferring handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = defer_pwchange use_first_pass debug
+ account = ignore_k5login debug
+ password = ignore_k5login use_first_pass debug
+ session = debug
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_NEW_AUTHTOK_REQD
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ DEBUG (user %u) krb5_get_init_creds_password: Password has expired
+ DEBUG (user %u) expired account, deferring failure
+ INFO user %u authenticated as %0 (expired)
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ INFO user %u account password is expired
+ DEBUG pam_sm_acct_mgmt: exit (failure)
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ INFO user %u changed Kerberos password
+ DEBUG (user %u) obtaining credentials with new password
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG /^\(user %u\) temporarily storing credentials in /tmp/krb5cc_pam_/
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG (user %u) retrieving principal from cache
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG /^\(user %u\) initializing ticket cache FILE:/tmp/krb5cc_/
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/expired/fail b/tests/data/scripts/expired/fail
new file mode 100644
index 000000000000..566b4b9c73dc
--- /dev/null
+++ b/tests/data/scripts/expired/fail
@@ -0,0 +1,20 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login fail_pwchange
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/expired/fail-debug b/tests/data/scripts/expired/fail-debug
new file mode 100644
index 000000000000..7f464b4ed89f
--- /dev/null
+++ b/tests/data/scripts/expired/fail-debug
@@ -0,0 +1,24 @@
+# Test default handling of expired passwords. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = ignore_k5login fail_pwchange debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %0
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/fast/anonymous b/tests/data/scripts/fast/anonymous
new file mode 100644
index 000000000000..5f725ae63dcf
--- /dev/null
+++ b/tests/data/scripts/fast/anonymous
@@ -0,0 +1,17 @@
+# Test anonymous FAST. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache anon_fast
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %0
diff --git a/tests/data/scripts/fast/anonymous-debug b/tests/data/scripts/fast/anonymous-debug
new file mode 100644
index 000000000000..48fd1eadd581
--- /dev/null
+++ b/tests/data/scripts/fast/anonymous-debug
@@ -0,0 +1,22 @@
+# Test FAST with an existing ticket cache, with debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache anon_fast debug
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) anonymous authentication for FAST succeeded
+ DEBUG /^\(user %u\) setting FAST credential cache to MEMORY:/
+ DEBUG (user %u) attempting authentication as %0
+ INFO user %u authenticated as %0
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/fast/ccache b/tests/data/scripts/fast/ccache
new file mode 100644
index 000000000000..32e5eaa92465
--- /dev/null
+++ b/tests/data/scripts/fast/ccache
@@ -0,0 +1,17 @@
+# Test FAST with an existing ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/fast/ccache-debug b/tests/data/scripts/fast/ccache-debug
new file mode 100644
index 000000000000..f3788f2fc1c7
--- /dev/null
+++ b/tests/data/scripts/fast/ccache-debug
@@ -0,0 +1,21 @@
+# Test FAST with an existing ticket cache, with debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0 debug
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) setting FAST credential cache to %0
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/fast/no-ccache b/tests/data/scripts/fast/no-ccache
new file mode 100644
index 000000000000..71d4e2d494cf
--- /dev/null
+++ b/tests/data/scripts/fast/no-ccache
@@ -0,0 +1,17 @@
+# Test FAST with an existing ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0BAD
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/fast/no-ccache-debug b/tests/data/scripts/fast/no-ccache-debug
new file mode 100644
index 000000000000..743ad5559538
--- /dev/null
+++ b/tests/data/scripts/fast/no-ccache-debug
@@ -0,0 +1,21 @@
+# Test FAST with an existing ticket cache, with debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache fast_ccache=%0BAD debug
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG /^\(user %u\) failed to get principal from FAST ccache %0BAD: /
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/long/password b/tests/data/scripts/long/password
new file mode 100644
index 000000000000..e8183976c004
--- /dev/null
+++ b/tests/data/scripts/long/password
@@ -0,0 +1,14 @@
+# Test authentication with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/long/password-debug b/tests/data/scripts/long/password-debug
new file mode 100644
index 000000000000..832c19340485
--- /dev/null
+++ b/tests/data/scripts/long/password-debug
@@ -0,0 +1,20 @@
+# Test excessively long password handling with debug logging. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/long/use-first b/tests/data/scripts/long/use-first
new file mode 100644
index 000000000000..b68800485d04
--- /dev/null
+++ b/tests/data/scripts/long/use-first
@@ -0,0 +1,14 @@
+# Test use_first_pass with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/long/use-first-debug b/tests/data/scripts/long/use-first-debug
new file mode 100644
index 000000000000..72747e81f40c
--- /dev/null
+++ b/tests/data/scripts/long/use-first-debug
@@ -0,0 +1,17 @@
+# Test use_first_pass with a long password and debug. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/no-cache/no-prompt b/tests/data/scripts/no-cache/no-prompt
new file mode 100644
index 000000000000..1eef2f26b4ee
--- /dev/null
+++ b/tests/data/scripts/no-cache/no-prompt
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/no-prompt-try b/tests/data/scripts/no-cache/no-prompt-try
new file mode 100644
index 000000000000..1d632a96f9e6
--- /dev/null
+++ b/tests/data/scripts/no-cache/no-prompt-try
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library w/try_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt try_first_pass
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/no-prompt-use b/tests/data/scripts/no-cache/no-prompt-use
new file mode 100644
index 000000000000..76ef388465d2
--- /dev/null
+++ b/tests/data/scripts/no-cache/no-prompt-use
@@ -0,0 +1,25 @@
+# Defer prompting to the Kerberos library w/use_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_prompt
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = /^(%u's Password|Password for %u): $/|%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/prompt b/tests/data/scripts/no-cache/prompt
new file mode 100644
index 000000000000..b0eb0d9ca57b
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt
@@ -0,0 +1,25 @@
+# Test basic auth w/prompting without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/prompt-expose b/tests/data/scripts/no-cache/prompt-expose
new file mode 100644
index 000000000000..a3365cc69754
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-expose
@@ -0,0 +1,25 @@
+# Test basic auth w/prompting without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = expose_account no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password for %u: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/prompt-fail b/tests/data/scripts/no-cache/prompt-fail
new file mode 100644
index 000000000000..376b0f911997
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-fail
@@ -0,0 +1,25 @@
+# Test failed password authentication. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |BAD%p
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/no-cache/prompt-fail-debug b/tests/data/scripts/no-cache/prompt-fail-debug
new file mode 100644
index 000000000000..9c9a7a406b4b
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-fail-debug
@@ -0,0 +1,36 @@
+# Test failed password authentication with debug logging. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache debug
+ account = no_ccache debug
+ session = no_ccache debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |BAD%p
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/no-cache/prompt-principal b/tests/data/scripts/no-cache/prompt-principal
new file mode 100644
index 000000000000..5e7278f1e92d
--- /dev/null
+++ b/tests/data/scripts/no-cache/prompt-principal
@@ -0,0 +1,26 @@
+# Test prompting for principal without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = prompt_principal no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_on = Principal: |%u
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/try-first b/tests/data/scripts/no-cache/try-first
new file mode 100644
index 000000000000..366801e9a078
--- /dev/null
+++ b/tests/data/scripts/no-cache/try-first
@@ -0,0 +1,25 @@
+# Test basic auth w/no AUTHTOK and try_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/no-cache/use-first b/tests/data/scripts/no-cache/use-first
new file mode 100644
index 000000000000..028009fd7ba7
--- /dev/null
+++ b/tests/data/scripts/no-cache/use-first
@@ -0,0 +1,25 @@
+# Test basic auth w/no AUTHTOK and use_first_pass. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pam-user/no-update b/tests/data/scripts/pam-user/no-update
new file mode 100644
index 000000000000..36520bb4f332
--- /dev/null
+++ b/tests/data/scripts/pam-user/no-update
@@ -0,0 +1,20 @@
+# PAM_USER updates disabled. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache no_update_user
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pam-user/update b/tests/data/scripts/pam-user/update
new file mode 100644
index 000000000000..11d404a02144
--- /dev/null
+++ b/tests/data/scripts/pam-user/update
@@ -0,0 +1,20 @@
+# PAM_USER updates. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %0 authenticated as %1
diff --git a/tests/data/scripts/password/authtok b/tests/data/scripts/password/authtok
new file mode 100644
index 000000000000..9f6a39935b2d
--- /dev/null
+++ b/tests/data/scripts/password/authtok
@@ -0,0 +1,21 @@
+# Test password change with new authtok set but not old. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/authtok-force b/tests/data/scripts/password/authtok-force
new file mode 100644
index 000000000000..3bc0b598521b
--- /dev/null
+++ b/tests/data/scripts/password/authtok-force
@@ -0,0 +1,18 @@
+# Test password change with new authtok set but not old. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok force_first_pass
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/authtok-too-long b/tests/data/scripts/password/authtok-too-long
new file mode 100644
index 000000000000..df81e24977b3
--- /dev/null
+++ b/tests/data/scripts/password/authtok-too-long
@@ -0,0 +1,17 @@
+# Test use_authtok with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+
+[output]
diff --git a/tests/data/scripts/password/authtok-too-long-debug b/tests/data/scripts/password/authtok-too-long-debug
new file mode 100644
index 000000000000..cb38e8861102
--- /dev/null
+++ b/tests/data/scripts/password/authtok-too-long-debug
@@ -0,0 +1,23 @@
+# Test use_authtok with an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = use_authtok debug
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+
+[output]
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ DEBUG pam_sm_chauthtok: exit (failure)
diff --git a/tests/data/scripts/password/banner b/tests/data/scripts/password/banner
new file mode 100644
index 000000000000..98c899c26af5
--- /dev/null
+++ b/tests/data/scripts/password/banner
@@ -0,0 +1,23 @@
+# Test password change with a modified banner. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = banner=realm
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current realm password: |%p
+ echo_off = Enter new realm password: |%n
+ echo_off = Retype new realm password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/banner-expose b/tests/data/scripts/password/banner-expose
new file mode 100644
index 000000000000..595fa0380b22
--- /dev/null
+++ b/tests/data/scripts/password/banner-expose
@@ -0,0 +1,23 @@
+# Test password change with banner and expose_account. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = expose_account banner=realm
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current realm password for %0: |%p
+ echo_off = Enter new realm password for %0: |%n
+ echo_off = Retype new realm password for %0: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/basic b/tests/data/scripts/password/basic
new file mode 100644
index 000000000000..5cb68267ce26
--- /dev/null
+++ b/tests/data/scripts/password/basic
@@ -0,0 +1,20 @@
+# Test password change with prompting. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/basic-debug b/tests/data/scripts/password/basic-debug
new file mode 100644
index 000000000000..ca1c86b9c2c9
--- /dev/null
+++ b/tests/data/scripts/password/basic-debug
@@ -0,0 +1,28 @@
+# Test password change with prompting and debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = debug
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ INFO user %u changed Kerberos password
+ DEBUG pam_sm_chauthtok: exit (success)
diff --git a/tests/data/scripts/password/expose b/tests/data/scripts/password/expose
new file mode 100644
index 000000000000..a82c1bd0b78d
--- /dev/null
+++ b/tests/data/scripts/password/expose
@@ -0,0 +1,23 @@
+# Test password change with prompting and expose_account. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = expose_account
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current Kerberos password for %0: |%p
+ echo_off = Enter new Kerberos password for %0: |%n
+ echo_off = Retype new Kerberos password for %0: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/ignore b/tests/data/scripts/password/ignore
new file mode 100644
index 000000000000..023cf5656f67
--- /dev/null
+++ b/tests/data/scripts/password/ignore
@@ -0,0 +1,18 @@
+# Test password prompt saving for ignored users. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = ignore_root
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_IGNORE
+ chauthtok(UPDATE_AUTHTOK) = PAM_IGNORE
+
+[prompts]
+ echo_off = Enter new password: |%n
+ echo_off = Retype new password: |%n
+
+[output]
diff --git a/tests/data/scripts/password/no-banner b/tests/data/scripts/password/no-banner
new file mode 100644
index 000000000000..9cabbd8ec5f9
--- /dev/null
+++ b/tests/data/scripts/password/no-banner
@@ -0,0 +1,23 @@
+# Test password change with no identifying banner. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = banner=
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current password: |%p
+ echo_off = Enter new password: |%n
+ echo_off = Retype new password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/no-banner-expose b/tests/data/scripts/password/no-banner-expose
new file mode 100644
index 000000000000..3a5b944887bd
--- /dev/null
+++ b/tests/data/scripts/password/no-banner-expose
@@ -0,0 +1,23 @@
+# Test password change with no banner and expose_account. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = expose_account banner=
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_off = Current password for %0: |%p
+ echo_off = Enter new password for %0: |%n
+ echo_off = Retype new password for %0: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/prompt-principal b/tests/data/scripts/password/prompt-principal
new file mode 100644
index 000000000000..1e7274eb058e
--- /dev/null
+++ b/tests/data/scripts/password/prompt-principal
@@ -0,0 +1,24 @@
+# Test password change with prompting and prompt_principal. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = prompt_principal
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_SUCCESS
+
+[prompts]
+ echo_on = Principal: |%u
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+ echo_off = Retype new Kerberos password: |%n
+
+[output]
+ INFO user %u changed Kerberos password
diff --git a/tests/data/scripts/password/too-long b/tests/data/scripts/password/too-long
new file mode 100644
index 000000000000..4dbabd5db11e
--- /dev/null
+++ b/tests/data/scripts/password/too-long
@@ -0,0 +1,15 @@
+# Test password change to an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+
+[output]
diff --git a/tests/data/scripts/password/too-long-debug b/tests/data/scripts/password/too-long-debug
new file mode 100644
index 000000000000..18b4ed608612
--- /dev/null
+++ b/tests/data/scripts/password/too-long-debug
@@ -0,0 +1,24 @@
+# Test password change to an excessively long password. -*- conf -*-
+#
+# Copyright 2020 Russ Allbery <eagle@eyrie.org>
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ password = debug
+
+[run]
+ chauthtok(PRELIM_CHECK) = PAM_SUCCESS
+ chauthtok(UPDATE_AUTHTOK) = PAM_AUTHTOK_ERR
+
+[prompts]
+ echo_off = Current Kerberos password: |%p
+ echo_off = Enter new Kerberos password: |%n
+
+[output]
+ DEBUG pam_sm_chauthtok: entry (prelim)
+ DEBUG (user %u) attempting authentication as %0 for kadmin/changepw
+ DEBUG pam_sm_chauthtok: exit (success)
+ DEBUG pam_sm_chauthtok: entry (update)
+ DEBUG /^\(user %u\) rejecting password longer than [0-9]+$/
+ DEBUG pam_sm_chauthtok: exit (failure)
diff --git a/tests/data/scripts/pkinit/basic b/tests/data/scripts/pkinit/basic
new file mode 100644
index 000000000000..713bf0af1ce1
--- /dev/null
+++ b/tests/data/scripts/pkinit/basic
@@ -0,0 +1,22 @@
+# Test PKINIT auth without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit pkinit_user=FILE:%0
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/basic-debug b/tests/data/scripts/pkinit/basic-debug
new file mode 100644
index 000000000000..92a3fcf934d6
--- /dev/null
+++ b/tests/data/scripts/pkinit/basic-debug
@@ -0,0 +1,30 @@
+# Test PKINIT auth without saving a ticket cache w/debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug no_ccache use_pkinit pkinit_user=FILE:%0
+ account = debug no_ccache
+ session = debug no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG pam_sm_acct_mgmt: exit (success)
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/pkinit/no-use-pkinit b/tests/data/scripts/pkinit/no-use-pkinit
new file mode 100644
index 000000000000..ead640bcc4a0
--- /dev/null
+++ b/tests/data/scripts/pkinit/no-use-pkinit
@@ -0,0 +1,18 @@
+# Test for unsupported use_pkinit. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ ERR use_pkinit requested but PKINIT not available or cannot be enforced
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/pkinit/pin-mit b/tests/data/scripts/pkinit/pin-mit
new file mode 100644
index 000000000000..9791ebc2ace6
--- /dev/null
+++ b/tests/data/scripts/pkinit/pin-mit
@@ -0,0 +1,20 @@
+# Test PKINIT auth with a PIN prompt. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit pkinit_user=PKCS12:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Pass phrase for %0: |%1
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/preauth-opt-mit b/tests/data/scripts/pkinit/preauth-opt-mit
new file mode 100644
index 000000000000..4602d18c7556
--- /dev/null
+++ b/tests/data/scripts/pkinit/preauth-opt-mit
@@ -0,0 +1,17 @@
+# Test PKINIT auth with MIT preauth options. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache try_pkinit preauth_opt=X509_user_identity=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/prompt-try b/tests/data/scripts/pkinit/prompt-try
new file mode 100644
index 000000000000..723a228847e3
--- /dev/null
+++ b/tests/data/scripts/pkinit/prompt-try
@@ -0,0 +1,20 @@
+# Test try_pkinit with an initial prompt. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache try_pkinit pkinit_user=FILE:%0 pkinit_prompt
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Insert smart card if desired, then press Enter: |
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/prompt-use b/tests/data/scripts/pkinit/prompt-use
new file mode 100644
index 000000000000..0b341d5d73ce
--- /dev/null
+++ b/tests/data/scripts/pkinit/prompt-use
@@ -0,0 +1,20 @@
+# Test use_pkinit with an initial prompt. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache use_pkinit pkinit_user=FILE:%0 pkinit_prompt
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[prompts]
+ echo_off = Insert smart card and press Enter: |
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/try-pkinit b/tests/data/scripts/pkinit/try-pkinit
new file mode 100644
index 000000000000..13b7bcf76653
--- /dev/null
+++ b/tests/data/scripts/pkinit/try-pkinit
@@ -0,0 +1,17 @@
+# Test optional PKINIT auth without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache try_pkinit pkinit_user=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/pkinit/try-pkinit-debug b/tests/data/scripts/pkinit/try-pkinit-debug
new file mode 100644
index 000000000000..c721395abd07
--- /dev/null
+++ b/tests/data/scripts/pkinit/try-pkinit-debug
@@ -0,0 +1,19 @@
+# Test optional PKINIT auth w/debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug no_ccache try_pkinit pkinit_user=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/pkinit/try-pkinit-debug-mit b/tests/data/scripts/pkinit/try-pkinit-debug-mit
new file mode 100644
index 000000000000..2c8c966bdc03
--- /dev/null
+++ b/tests/data/scripts/pkinit/try-pkinit-debug-mit
@@ -0,0 +1,20 @@
+# Test optional PKINIT auth w/debug. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = debug no_ccache try_pkinit pkinit_user=FILE:%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u
+ INFO user %u authenticated as %u
+ DEBUG pam_sm_authenticate: exit (success)
diff --git a/tests/data/scripts/realm/fail-bad-user-realm b/tests/data/scripts/realm/fail-bad-user-realm
new file mode 100644
index 000000000000..d30bec6f1f33
--- /dev/null
+++ b/tests/data/scripts/realm/fail-bad-user-realm
@@ -0,0 +1,17 @@
+# Test authentication failure with different user_realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache user_realm=%0
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/fail-no-realm b/tests/data/scripts/realm/fail-no-realm
new file mode 100644
index 000000000000..87b59aab49f2
--- /dev/null
+++ b/tests/data/scripts/realm/fail-no-realm
@@ -0,0 +1,17 @@
+# Test authentication failure due to wrong realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/fail-no-realm-debug b/tests/data/scripts/realm/fail-no-realm-debug
new file mode 100644
index 000000000000..5ef2ce588177
--- /dev/null
+++ b/tests/data/scripts/realm/fail-no-realm-debug
@@ -0,0 +1,21 @@
+# Test authentication failure due to wrong realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache debug
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) attempting authentication as %u@%0
+ DEBUG /^\(user %u\) krb5_get_init_creds_password: /
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
diff --git a/tests/data/scripts/realm/fail-realm b/tests/data/scripts/realm/fail-realm
new file mode 100644
index 000000000000..6dfe6a044354
--- /dev/null
+++ b/tests/data/scripts/realm/fail-realm
@@ -0,0 +1,17 @@
+# Test authentication failure with different realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache realm=%0
+
+[run]
+ authenticate = PAM_AUTHINFO_UNAVAIL
+
+[output]
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/fail-user-realm b/tests/data/scripts/realm/fail-user-realm
new file mode 100644
index 000000000000..c97324c2d028
--- /dev/null
+++ b/tests/data/scripts/realm/fail-user-realm
@@ -0,0 +1,18 @@
+# Test authentication failure with different user_realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache user_realm=%0
+
+[run]
+ authenticate = PAM_AUTH_ERR
+
+[output]
+ ERR /^\(user %u\) cannot convert principal to user: /
+ NOTICE failed authorization check; logname=%u uid=%i euid=%i tty= ruser= rhost=
diff --git a/tests/data/scripts/realm/pass-realm b/tests/data/scripts/realm/pass-realm
new file mode 100644
index 000000000000..91136c9bfc1c
--- /dev/null
+++ b/tests/data/scripts/realm/pass-realm
@@ -0,0 +1,17 @@
+# Test authentication success with different realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache realm=%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u@%0
diff --git a/tests/data/scripts/realm/pass-user-realm b/tests/data/scripts/realm/pass-user-realm
new file mode 100644
index 000000000000..86007c2d4d26
--- /dev/null
+++ b/tests/data/scripts/realm/pass-user-realm
@@ -0,0 +1,17 @@
+# Test authentication success with different user_realm. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache user_realm=%0
+
+[run]
+ authenticate = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u@%0
diff --git a/tests/data/scripts/stacked/auth-only b/tests/data/scripts/stacked/auth-only
new file mode 100644
index 000000000000..46d3308ac0e4
--- /dev/null
+++ b/tests/data/scripts/stacked/auth-only
@@ -0,0 +1,18 @@
+# Test basic authentication without setcred. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/basic b/tests/data/scripts/stacked/basic
new file mode 100644
index 000000000000..a05640d278bf
--- /dev/null
+++ b/tests/data/scripts/stacked/basic
@@ -0,0 +1,22 @@
+# Test basic authentication without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/prompt b/tests/data/scripts/stacked/prompt
new file mode 100644
index 000000000000..b0eb0d9ca57b
--- /dev/null
+++ b/tests/data/scripts/stacked/prompt
@@ -0,0 +1,25 @@
+# Test basic auth w/prompting without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_off = Password: |%p
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/prompt-principal b/tests/data/scripts/stacked/prompt-principal
new file mode 100644
index 000000000000..b416671875c7
--- /dev/null
+++ b/tests/data/scripts/stacked/prompt-principal
@@ -0,0 +1,25 @@
+# Test prompting for principal without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = prompt_principal force_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[prompts]
+ echo_on = Principal: |%u
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/try-first b/tests/data/scripts/stacked/try-first
new file mode 100644
index 000000000000..3a14b7584bc1
--- /dev/null
+++ b/tests/data/scripts/stacked/try-first
@@ -0,0 +1,22 @@
+# Test try_first_pass without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = try_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/stacked/use-first b/tests/data/scripts/stacked/use-first
new file mode 100644
index 000000000000..29c5c5c4188d
--- /dev/null
+++ b/tests/data/scripts/stacked/use-first
@@ -0,0 +1,22 @@
+# Test use_first_pass without saving a ticket cache. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = use_first_pass no_ccache
+ account = no_ccache
+ session = no_ccache
+
+[run]
+ authenticate = PAM_SUCCESS
+ acct_mgmt = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ INFO user %u authenticated as %u
diff --git a/tests/data/scripts/trace/supported b/tests/data/scripts/trace/supported
new file mode 100644
index 000000000000..f67c389735ff
--- /dev/null
+++ b/tests/data/scripts/trace/supported
@@ -0,0 +1,58 @@
+# Basic test of enabling trace logging. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache trace=%0 debug
+ account = no_ccache trace=%0 debug
+ session = no_ccache trace=%0 debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) no stored password
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (delete)
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (establish)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (refresh)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_setcred: entry (reinit)
+ DEBUG no context found, creating one
+ DEBUG (user root) unable to get PAM_KRB5CCNAME, assuming non-Kerberos login
+ DEBUG pam_sm_setcred: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ DEBUG enabled trace logging to %0
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/scripts/trace/unsupported b/tests/data/scripts/trace/unsupported
new file mode 100644
index 000000000000..2100c34fc2f5
--- /dev/null
+++ b/tests/data/scripts/trace/unsupported
@@ -0,0 +1,52 @@
+# Basic test of attempting trace logging when not supported. -*- conf -*-
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2010-2011
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# SPDX-License-Identifier: BSD-3-clause or GPL-1+
+
+[options]
+ auth = force_first_pass no_ccache trace=%0 debug
+ account = no_ccache trace=%0 debug
+ session = no_ccache trace=%0 debug
+
+[run]
+ authenticate = PAM_AUTH_ERR
+ acct_mgmt = PAM_IGNORE
+ setcred(DELETE_CRED) = PAM_SUCCESS
+ setcred(ESTABLISH_CRED) = PAM_SUCCESS
+ setcred(REFRESH_CRED) = PAM_SUCCESS
+ setcred(REINITIALIZE_CRED) = PAM_SUCCESS
+ open_session = PAM_SUCCESS
+ close_session = PAM_SUCCESS
+
+[output]
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_authenticate: entry
+ DEBUG (user %u) no stored password
+ NOTICE authentication failure; logname=%u uid=%i euid=%i tty= ruser= rhost=
+ DEBUG pam_sm_authenticate: exit (failure)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_acct_mgmt: entry
+ DEBUG skipping non-Kerberos login
+ DEBUG pam_sm_acct_mgmt: exit (ignore)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (delete)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (establish)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (refresh)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_setcred: entry (reinit)
+ DEBUG pam_sm_setcred: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_open_session: entry
+ DEBUG pam_sm_open_session: exit (success)
+ ERR trace logging requested but not supported
+ DEBUG pam_sm_close_session: entry
+ DEBUG pam_sm_close_session: exit (success)
diff --git a/tests/data/valgrind.supp b/tests/data/valgrind.supp
new file mode 100644
index 000000000000..6e987803f5e2
--- /dev/null
+++ b/tests/data/valgrind.supp
@@ -0,0 +1,242 @@
+# -*- conf -*-
+#
+# This is a valgrind suppression file for analysis of test suite results.
+#
+# Suppress a variety of apparent memory leaks in various Kerberos
+# implementations due to one-time instantiation of data, and a few other
+# artifacts of the test suite for rra-c-util portability and utility code
+# and related software.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2017-2018, 2020 Russ Allbery <eagle@eyrie.org>
+# Copyright 2011-2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+{
+ dlopen-dlerror
+ Memcheck:Leak
+ fun:calloc
+ fun:_dlerror_run
+}
+{
+ fakeroot-msgsnd
+ Memcheck:Param
+ msgsnd(msgp->mtext)
+ fun:msgsnd
+ fun:send_fakem
+ fun:send_get_fakem
+ obj:*/libfakeroot-sysv.so
+}
+{
+ heimdal-base-once
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:heim_base_once_f
+}
+{
+ heimdal-gss-config
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_config_parse_debug
+}
+{
+ heimdal-gss-config-2
+ Memcheck:Leak
+ fun:*alloc
+ fun:_krb5_config_get_entry
+}
+{
+ heimdal-gss-cred
+ Memcheck:Leak
+ fun:calloc
+ obj:*libgssapi.so.*
+ obj:*libgssapi.so.*
+ fun:gss_acquire_cred
+}
+{
+ heimdal-gss-krb5-init
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:_gsskrb5_init
+}
+{
+ heimdal-gss-load-mech
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:_gss_load_mech
+}
+{
+ heimdal-krb5-init-context-once
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:init_context_once
+}
+{
+ heimdal-krb5-reg-plugins-once
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_plugin_register
+ fun:reg_def_plugins_once
+}
+{
+ heimdal-krb5-openssl-init
+ Memcheck:Leak
+ fun:*alloc
+ obj:*
+ fun:CRYPTO_*alloc
+}
+{
+ mit-gss-ccache
+ Memcheck:Leak
+ fun:*alloc
+ fun:krb5int_setspecific
+ fun:kg_set_ccache_name
+ fun:gss_krb5int_ccache_name
+}
+{
+ mit-gss-ccache-2
+ Memcheck:Leak
+ fun:*alloc
+ fun:strdup
+ fun:kg_set_ccache_name
+ fun:gss_krb5int_ccache_name
+}
+{
+ mit-gss-error
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_gss_save_error_string
+}
+{
+ mit-gss-mechs
+ Memcheck:Leak
+ fun:glob
+ fun:loadConfigFiles
+ fun:updateMechList
+ fun:build_mechSet
+ fun:gss_indicate_mechs
+}
+{
+ mit-kadmin-ovku-error
+ Memcheck:Leak
+ fun:*alloc*
+ fun:initialize_ovku_error_table_r
+}
+{
+ mit-krb5-changepw
+ Memcheck:Leak
+ fun:*alloc
+ fun:change_set_password
+ fun:krb5_change_password
+ fun:krb5_get_init_creds_password
+}
+{
+ mit-krb5-pkinit-openssl-init
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_init_preauth_context
+}
+{
+ mit-krb5-pkinit-openssl-request
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:krb5_preauth_request_context_init
+}
+{
+ mit-krb5-pkinit-openssl-request-2
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:k5_preauth_request_context_init
+}
+{
+ mit-krb5-plugin-dirs
+ Memcheck:Leak
+ fun:calloc
+ fun:krb5int_open_plugin_dirs
+}
+{
+ mit-krb5-plugin-dlerror
+ Memcheck:Leak
+ fun:calloc
+ fun:_dlerror_run
+ ...
+ fun:krb5int_open_plugin
+}
+{
+ mit-krb5-plugin-register
+ Memcheck:Leak
+ fun:malloc
+ fun:strdup
+ fun:register_module.isra.1
+}
+{
+ mit-krb5-preauth-init
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:k5_init_preauth_context
+}
+{
+ mit-krb5-preauth-init
+ Memcheck:Leak
+ fun:strdup
+ fun:add_to_list
+ fun:profile_get_values
+ ...
+ fun:clpreauth_prep_questions
+}
+{
+ mit-krb5-preauth-init-2
+ Memcheck:Leak
+ fun:*alloc
+ fun:init_list
+ fun:profile_get_values
+ ...
+ fun:clpreauth_prep_questions
+}
+{
+ mit-krb5-profile
+ Memcheck:Leak
+ fun:*alloc
+ ...
+ fun:profile_open_file
+}
+{
+ portable-setenv
+ Memcheck:Leak
+ fun:malloc
+ fun:test_setenv
+}
diff --git a/tests/docs/pod-spelling-t b/tests/docs/pod-spelling-t
new file mode 100755
index 000000000000..2ea5bf3ba6ee
--- /dev/null
+++ b/tests/docs/pod-spelling-t
@@ -0,0 +1,55 @@
+#!/usr/bin/perl
+#
+# Checks all POD files in the tree for spelling errors using Test::Spelling.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2016, 2019, 2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2012-2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_author use_prereq);
+use Test::RRA::Automake qw(automake_setup perl_dirs);
+
+use Test::More;
+
+# Only run this test for the module author since the required stopwords are
+# too sensitive to the exact spell-checking program and dictionary.
+skip_unless_author('Spelling tests');
+
+# Load prerequisite modules.
+use_prereq('Test::Spelling');
+
+# Set up Automake testing.
+automake_setup();
+
+# Run the tests.
+all_pod_files_spelling_ok(perl_dirs());
diff --git a/tests/docs/pod-t b/tests/docs/pod-t
new file mode 100755
index 000000000000..be21ddf01cea
--- /dev/null
+++ b/tests/docs/pod-t
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+#
+# Check all POD documents in the tree, except for any embedded Perl module
+# distribution, for POD formatting errors.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2016, 2019, 2021 Russ Allbery <eagle@eyrie.org>
+# Copyright 2012-2014
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_automated use_prereq);
+use Test::RRA::Automake qw(automake_setup perl_dirs);
+
+use Test::More;
+
+# Skip this test for normal user installs, since we normally pre-generate all
+# of the documentation and the end user doesn't care.
+skip_unless_automated('POD syntax tests');
+
+# Load prerequisite modules.
+use_prereq('Test::Pod');
+
+# Set up Automake testing.
+automake_setup();
+
+# Run the tests.
+all_pod_files_ok(perl_dirs());
diff --git a/tests/docs/spdx-license-t b/tests/docs/spdx-license-t
new file mode 100755
index 000000000000..2841835fb69a
--- /dev/null
+++ b/tests/docs/spdx-license-t
@@ -0,0 +1,149 @@
+#!/usr/bin/perl
+#
+# Check source files for SPDX-License-Identifier fields.
+#
+# Examine all source files in a distribution to check that they contain an
+# SPDX-License-Identifier field. This does not check the syntax or whether
+# the identifiers are valid.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Copyright 2018-2021 Russ Allbery <eagle@eyrie.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_automated);
+use Test::RRA::Automake qw(all_files automake_setup);
+
+use File::Basename qw(basename);
+use Test::More;
+
+# File name (the file without any directory component) and path patterns to
+# skip for this check.
+## no critic (RegularExpressions::ProhibitFixedStringMatches)
+my @IGNORE = (
+ qr{ \A LICENSE \z }xms, # Generated file with no license itself
+ qr{ \A (NEWS|THANKS|TODO) \z }xms, # Package license should be fine
+ qr{ \A README ( [.] .* )? \z }xms, # Package license should be fine
+ qr{ \A (Makefile|libtool) \z }xms, # Generated file
+ qr{ ~ \z }xms, # Backup files
+ qr{ [.] l?a \z }xms, # Created by libtool
+ qr{ [.] o \z }xms, # Compiler objects
+ qr{ [.] output \z }xms, # Test data
+);
+my @IGNORE_PATHS = (
+ qr{ \A debian/ }xms, # Found in debian/* branches
+ qr{ \A docs/metadata/ }xms, # Package license should be fine
+ qr{ \A docs/protocol[.](html|txt) \z }xms, # Generated by xml2rfc
+ qr{ \A m4/ (libtool|lt.*) [.] m4 \z }xms, # Files from Libtool
+ qr{ \A perl/Build \z }xms, # Perl build files
+ qr{ \A perl/MANIFEST \z }xms, # Perl build files
+ qr{ \A perl/MYMETA [.] }xms, # Perl build files
+ qr{ \A perl/blib/ }xms, # Perl build files
+ qr{ \A perl/cover_db/ }xms, # Perl test files
+ qr{ \A perl/_build }xms, # Perl build files
+ qr{ \A php/Makefile [.] global \z }xms, # Created by phpize
+ qr{ \A php/autom4te [.] cache/ }xms, # Created by phpize
+ qr{ \A php/acinclude [.] m4 \z }xms, # Created by phpize
+ qr{ \A php/build/ }xms, # Created by phpize
+ qr{ \A php/config [.] (guess|sub) \z }xms, # Created by phpize
+ qr{ \A php/configure [.] (ac|in) \z }xms, # Created by phpize
+ qr{ \A php/ltmain [.] sh \z }xms, # Created by phpize
+ qr{ \A php/run-tests [.] php \z }xms, # Created by phpize
+ qr{ \A python/ .* [.] egg-info/ }xms, # Python build files
+ qr{ \A tests/config/ (?!README) }xms, # Test configuration
+ qr{ \A tests/tmp/ }xms, # Temporary test files
+);
+## use critic
+
+# Only run this test during automated testing, since failure doesn't indicate
+# any user-noticable flaw in the package itself.
+skip_unless_automated('SPDX identifier tests');
+
+# Set up Automake testing.
+automake_setup();
+
+# Check a single file for an occurrence of the string.
+#
+# $path - Path to the file
+#
+# Returns: undef
+sub check_file {
+ my ($path) = @_;
+ my $filename = basename($path);
+
+ # Ignore files in the whitelist and binary files.
+ for my $pattern (@IGNORE) {
+ return if $filename =~ $pattern;
+ }
+ for my $pattern (@IGNORE_PATHS) {
+ return if $path =~ $pattern;
+ }
+ return if !-T $path;
+
+ # Scan the file.
+ my ($saw_legacy_notice, $saw_spdx, $skip_spdx);
+ open(my $file, '<', $path) or BAIL_OUT("Cannot open $path: $!");
+ while (defined(my $line = <$file>)) {
+ if ($line =~ m{ Generated [ ] by [ ] libtool [ ] }xms) {
+ close($file) or BAIL_OUT("Cannot close $path: $!");
+ return;
+ }
+ if ($line =~ m{ \b See \s+ LICENSE \s+ for \s+ licensing }xms) {
+ $saw_legacy_notice = 1;
+ }
+ if ($line =~ m{ \b SPDX-License-Identifier: \s+ \S+ }xms) {
+ $saw_spdx = 1;
+ last;
+ }
+ if ($line =~ m{ no \s SPDX-License-Identifier \s registered }xms) {
+ $skip_spdx = 1;
+ last;
+ }
+ }
+ close($file) or BAIL_OUT("Cannot close $path: $!");
+
+ # If there is a legacy license notice, report a failure regardless of file
+ # size. Otherwise, skip files under 1KB. They can be rolled up into the
+ # overall project license and the license notice may be a substantial
+ # portion of the file size.
+ if ($saw_legacy_notice) {
+ ok(!$saw_legacy_notice, "$path has legacy license notice");
+ } else {
+ ok($saw_spdx || $skip_spdx || -s $path < 1024, $path);
+ }
+ return;
+}
+
+# Scan every file. We don't declare a plan since we skip a lot of files and
+# don't want to precalculate the file list.
+my @paths = all_files();
+for my $path (@paths) {
+ check_file($path);
+}
+done_testing();
diff --git a/tests/fakepam/README b/tests/fakepam/README
new file mode 100644
index 000000000000..f7522cc1d66e
--- /dev/null
+++ b/tests/fakepam/README
@@ -0,0 +1,276 @@
+ PAM Testing Framework
+
+Overview
+
+ The files in this directory provide a shim PAM library that's used for
+ testing and a test framework used to exercise a PAM module.
+
+ This library and its include files define the minimum amount
+ of the PAM module interface so that PAM modules can be tested without
+ such problems as needing configuration files in /etc/pam.d or needing
+ changes to the system configuration to run a testing PAM module
+ instead of the normal system PAM modules.
+
+ The goal of this library is that all PAM code should be able to be
+ left unchanged and the code just linked with the fakepam library
+ rather than the regular PAM library. The testing code can then call
+ pam_start and pam_end as defined in the fakepam/pam.h header file and
+ inspect internal PAM state as needed.
+
+ The library also provides an interface to exercise a PAM module via an
+ interaction script, so that as much of the testing process as possible
+ is moved into simple text files instead of C code. That test script
+ format supports specifying the PAM configuration, the PAM interfaces
+ to run, the expected prompts and replies, and the expected log
+ messages. That interface is defined in fakepam/script.h.
+
+Fake PAM Library
+
+ Unfortunately, the standard PAM library for most operating systems
+ does not provide a reasonable testing framework. The primary problem
+ is configuration: the PAM library usually hard-codes a configuration
+ location such as /etc/pam.conf or /etc/pam.d/<application>. But there
+ are other problems as well, such as capturing logging rather than
+ having it go to syslog and inspecting PAM internal state to make sure
+ that it's updated properly by the module.
+
+ This library implements some of the same API as the system PAM library
+ and uses the system PAM library headers, but the underlying
+ implementation does not call the system PAM library or dynamically
+ load modules. Instead, it's meant to be linked into a single
+ executable along with the implementation of a PAM module. It does not
+ provide most of the application-level PAM interfaces (so one cannot
+ link a PAM-using application against it), just the interfaces called
+ by a module. The caller of the library can then call the module API
+ (such as pam_sm_authenticate) directly.
+
+ All of the internal state maintained by the PAM library is made
+ available to the test program linked with this library. See
+ fakepam/pam.h for the data structures. This allows verification that
+ the PAM module is setting the internal PAM state properly.
+
+ User Handling
+
+ In order to write good test suites, one often has to be able to
+ authenticate as a variety of users, but PAM modules may expect the
+ authenticating user to exist on the system. The fakepam library
+ provides a pam_modutil_getpwnam (if available) or a getpwnam
+ implementation that returns information for a single user (and user
+ unknown for everyone else). To set the information for the one valid
+ user, call the pam_set_pwd function and provide a struct passwd that
+ will be returned by pam_modutil_getpwnam.
+
+ The fakepam library also provides a replacement krb5_kuserok function
+ for testing PAM modules that use Kerberos. This source file should
+ only be included in packages that are building with Kerberos. It
+ implements the same functionality as the default krb5_kuserok
+ function, but looks for .k5login in the home directory configured by
+ the test framework instead of using getpwnam.
+
+ Only those two functions are intercepted, so if the module looks up
+ users in other ways, it may still bypass the fakepam library and look
+ at system users.
+
+ Output Handling
+
+ The fakepam library intercepts the PAM functions that would normally
+ log to syslog and instead accumulates the output in a static string
+ variable. To retrieve the logging output so far, call pam_output,
+ which returns a struct of all the output strings up to that point and
+ resets the accumulated output.
+
+Scripted PAM Testing
+
+ Also provided as part of the fakepam library is a test framework for
+ testing PAM modules. This test framework allows most of the testing
+ process to be encapsulated in a text configuration file per test,
+ rather than in a tedious set of checks and calls written in C.
+
+ Test API
+
+ The basic test API is to call either run_script (to run a single test
+ script) or run_script_dir (to run all scripts in a particular
+ directory). Both take a configuration struct that controls how the
+ PAM library is set up and called.
+
+ That configuration struct takes the following elements:
+
+ user
+ The user as which to authenticate, passed into pam_start and also
+ substituted for the %u escape. This should match the user whose
+ home directory information is configured using pam_set_pwd if that
+ function is in use.
+
+ password
+ Only used for the %p escape. This is not used to set the
+ authentication token in the PAM library (see authtok below).
+
+ newpass
+ Only used for the %n escape.
+
+ extra
+ An array of up to 10 additional strings used by the %0 through %9
+ escapes when parsing the configuration file, as discussed below.
+
+ authtok
+ Sets the default value of the PAM_AUTHTOK data item. This will be
+ set immediately after initializing the PAM library and before
+ calling any PAM module functions.
+
+ authtok
+ Like authtok, but for the PAM_OLDAUTHTOK data item.
+
+ callback
+ This, and the associated data element, specifies a callback that's
+ called at the end of processing of the script before calling
+ pam_end. This can be used to inspect and verify the internal
+ state of PAM. The data element is an opaque pointer passed into
+ the callback.
+
+ Test Script Basic Format
+
+ Test scripts are composed of one or more sections. Each section
+ begins with:
+
+ [<section>]
+
+ starting in column 1, where <section> is the name of the section. The
+ valid section types and the format of their contents are described
+ below.
+
+ Blank lines and lines starting with # are ignored.
+
+ Several strings undergo %-escape expansion as mentioned below. For
+ any such string, the following escapes are supported:
+
+ %i Current UID (not the UID of the target user)
+ %n New password
+ %p Password
+ %u Username
+ %0 extra[0]
+ ...
+ %9 extra[9]
+
+ All of these are set in the script_config struct.
+
+ Regular expression matching is supported for output lines and for
+ prompts. To mark an expected prompt or output line as a regular
+ expression, it must begin and end with a slash (/). Slashes inside
+ the regular expression do not need to be escaped. If regular
+ expression support is not available in the C library, those matching
+ tests will be skipped.
+
+ The [options] Section
+
+ The [options] section contains the PAM configuration that will be
+ passed to the module. These are the options that are normally listed
+ in the PAM configuration file after the name of the module. The
+ syntax of this section is one or more lines of the form:
+
+ <group> = <options>
+
+ where <group> is one of "account", "auth", "password", or "session".
+ The options are space-delimited and may be either option names or
+ option=value pairs.
+
+ The [run] Section
+
+ The [run] section specifies what PAM interfaces to call. It consists
+ of one or more lines in the format:
+
+ <call> = <status>
+
+ where <call> is the PAM call to make and <status> is the status code
+ that it should return. <call> is one of the PAM module interface
+ functions without the leading "pam_sm_", so one of "acct_mgmt",
+ "authenticate", "setcred", "chauthtok", "open_session", or
+ "close_session". The return status is one of the PAM constants
+ defined for return status, such as PAM_IGNORE or PAM_SUCCESS. The
+ test framework will ensure that the PAM call returns the appropriate
+ status.
+
+ The <call> may be optionally followed by an open parentheses and then
+ a list of flags separated by |, or syntactically:
+
+ <call>(<flag>|<flag>|...) = <status>
+
+ In this form, rather than passing a flags value of 0 to the PAM call,
+ the test framework will pass the combination of the provided flags.
+ The flags are PAM constants without the leading PAM_, so (for example)
+ DELETE_CRED, ESTABLISH_CRED, REFRESH_CRED, or REINITIALIZE_CRED for
+ the "setcred" call.
+
+ As a special case, <call> may be "end" to specify flags to pass to the
+ pam_end call (such as PAM_DATA_SILENT).
+
+ The [end] Section
+
+ The [end] section defines how to call pam_end. It currently takes
+ only one setting, flags, the syntax of which is:
+
+ flags = <flag>|<flag>
+
+ This allows PAM_DATA_SILENT or other flags to be passed to pam_end
+ when running the test script.
+
+ The [output] Section
+
+ The [output] section defines the logging output expected from the
+ module. It consists of zero or more lines in the format:
+
+ <priority> <output>
+ <priority> /<regex>/
+
+ where <priority> is a syslog priority and <output> is the remaining
+ output or a regular expression to match against the output. Valid
+ values for <priority> are DEBUG, INFO, NOTICE, ERR, and CRIT.
+ <output> and <regex> may contain spaces and undergoes %-escape
+ expansion.
+
+ The replacement values are taken from the script_config struct passed
+ as a parameter to run_script or run_script_dir.
+
+ If the [output] section is missing entirely, the test framework will
+ expect there to be no logging output from the PAM module.
+
+ This defines the logging output, not the prompts returned through the
+ conversation function. For that, see the next section.
+
+ The [prompts] Section
+
+ The [prompts] section defines the prompts that the PAM module is
+ expected to send via the conversation function, and the responses that
+ the test harness will send back (if any). This consists of zero or
+ more lines in one of the following formats:
+
+ <type> = <prompt>
+ <type> = /<prompt>/
+ <type> = <prompt>|<response>
+ <type> = /<prompt>/|<response>
+
+ The <type> is the style of prompt, chosen from "echo_off", "echo_on",
+ "error_msg", and "info". The <prompt> is the actual prompt sent and
+ undergoes %-escape expansion. It may be enclosed in slashes (/) to
+ indicate that it's a regular expression instead of literal text. The
+ <response> if present (and its presence is signaled by the |
+ character) contains the response sent back by the test framework and
+ also undergoes %-escape expansion. The response starts with the final
+ | character on the line, so <prompt> regular expressions may freely
+ use | inside the regular expression.
+
+ If the [prompts] section is present and empty, the test harness will
+ check that the PAM module does not send any prompts. If the [prompts]
+ section is absent entirely, the conversation function passed to the
+ PAM module will be NULL.
+
+License
+
+ This file is part of the documentation of rra-c-util, which can be
+ found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+ Copyright 2011-2012, 2020-2021 Russ Allbery <eagle@eyrie.org>
+
+ Copying and distribution of this file, with or without modification,
+ are permitted in any medium without royalty provided the copyright
+ notice and this notice are preserved. This file is offered as-is,
+ without any warranty.
diff --git a/tests/fakepam/config.c b/tests/fakepam/config.c
new file mode 100644
index 000000000000..8e0685604d55
--- /dev/null
+++ b/tests/fakepam/config.c
@@ -0,0 +1,766 @@
+/*
+ * Run a PAM interaction script for testing.
+ *
+ * Provides an interface that loads a PAM interaction script from a file and
+ * runs through that script, calling the internal PAM module functions and
+ * checking their results. This allows automation of PAM testing through
+ * external data files instead of coding everything in C.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017-2018, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <syslog.h>
+
+#include <tests/fakepam/internal.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* Used for enumerating arrays. */
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+/* Mapping of strings to PAM function pointers and group numbers. */
+static const struct {
+ const char *name;
+ pam_call call;
+ enum group_type group;
+} CALLS[] = {
+ /* clang-format off */
+ {"acct_mgmt", pam_sm_acct_mgmt, GROUP_ACCOUNT },
+ {"authenticate", pam_sm_authenticate, GROUP_AUTH },
+ {"setcred", pam_sm_setcred, GROUP_AUTH },
+ {"chauthtok", pam_sm_chauthtok, GROUP_PASSWORD},
+ {"open_session", pam_sm_open_session, GROUP_SESSION },
+ {"close_session", pam_sm_close_session, GROUP_SESSION },
+ /* clang-format on */
+};
+
+/* Mapping of PAM flag names without the leading PAM_ to values. */
+static const struct {
+ const char *name;
+ int value;
+} FLAGS[] = {
+ /* clang-format off */
+ {"CHANGE_EXPIRED_AUTHTOK", PAM_CHANGE_EXPIRED_AUTHTOK},
+ {"DELETE_CRED", PAM_DELETE_CRED },
+ {"DISALLOW_NULL_AUTHTOK", PAM_DISALLOW_NULL_AUTHTOK },
+ {"ESTABLISH_CRED", PAM_ESTABLISH_CRED },
+ {"PRELIM_CHECK", PAM_PRELIM_CHECK },
+ {"REFRESH_CRED", PAM_REFRESH_CRED },
+ {"REINITIALIZE_CRED", PAM_REINITIALIZE_CRED },
+ {"SILENT", PAM_SILENT },
+ {"UPDATE_AUTHTOK", PAM_UPDATE_AUTHTOK },
+ /* clang-format on */
+};
+
+/* Mapping of strings to PAM groups. */
+static const struct {
+ const char *name;
+ enum group_type group;
+} GROUPS[] = {
+ /* clang-format off */
+ {"account", GROUP_ACCOUNT },
+ {"auth", GROUP_AUTH },
+ {"password", GROUP_PASSWORD},
+ {"session", GROUP_SESSION },
+ /* clang-format on */
+};
+
+/* Mapping of strings to PAM return values. */
+static const struct {
+ const char *name;
+ int status;
+} RETURNS[] = {
+ /* clang-format off */
+ {"PAM_AUTH_ERR", PAM_AUTH_ERR },
+ {"PAM_AUTHINFO_UNAVAIL", PAM_AUTHINFO_UNAVAIL},
+ {"PAM_AUTHTOK_ERR", PAM_AUTHTOK_ERR },
+ {"PAM_DATA_SILENT", PAM_DATA_SILENT },
+ {"PAM_IGNORE", PAM_IGNORE },
+ {"PAM_NEW_AUTHTOK_REQD", PAM_NEW_AUTHTOK_REQD},
+ {"PAM_SESSION_ERR", PAM_SESSION_ERR },
+ {"PAM_SUCCESS", PAM_SUCCESS },
+ {"PAM_USER_UNKNOWN", PAM_USER_UNKNOWN },
+ /* clang-format on */
+};
+
+/* Mapping of PAM prompt styles to their values. */
+static const struct {
+ const char *name;
+ int style;
+} STYLES[] = {
+ /* clang-format off */
+ {"echo_off", PAM_PROMPT_ECHO_OFF},
+ {"echo_on", PAM_PROMPT_ECHO_ON },
+ {"error_msg", PAM_ERROR_MSG },
+ {"info", PAM_TEXT_INFO },
+ /* clang-format on */
+};
+
+/* Mappings of strings to syslog priorities. */
+static const struct {
+ const char *name;
+ int priority;
+} PRIORITIES[] = {
+ /* clang-format off */
+ {"DEBUG", LOG_DEBUG },
+ {"INFO", LOG_INFO },
+ {"NOTICE", LOG_NOTICE},
+ {"ERR", LOG_ERR },
+ {"CRIT", LOG_CRIT },
+ /* clang-format on */
+};
+
+
+/*
+ * Given a pointer to a string, skip any leading whitespace and return a
+ * pointer to the first non-whitespace character.
+ */
+static char *
+skip_whitespace(char *p)
+{
+ while (isspace((unsigned char) (*p)))
+ p++;
+ return p;
+}
+
+
+/*
+ * Read a line from a file into a BUFSIZ buffer, failing if the line was too
+ * long to fit into the buffer, and returns a copy of that line in newly
+ * allocated memory. Ignores blank lines and comments. Caller is responsible
+ * for freeing. Returns NULL on end of file and fails on read errors.
+ */
+static char *
+readline(FILE *file)
+{
+ char buffer[BUFSIZ];
+ char *line, *first;
+
+ do {
+ line = fgets(buffer, sizeof(buffer), file);
+ if (line == NULL) {
+ if (feof(file))
+ return NULL;
+ sysbail("cannot read line from script");
+ }
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("script line too long");
+ buffer[strlen(buffer) - 1] = '\0';
+ first = skip_whitespace(buffer);
+ } while (first[0] == '#' || first[0] == '\0');
+ line = bstrdup(buffer);
+ return line;
+}
+
+
+/*
+ * Given the name of a PAM call, map it to a call enum. This is used later in
+ * switch statements to determine which function to call. Fails on any
+ * unrecognized string. If the optional second argument is not NULL, also
+ * store the group number in that argument.
+ */
+static pam_call
+string_to_call(const char *name, enum group_type *group)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(CALLS); i++)
+ if (strcmp(name, CALLS[i].name) == 0) {
+ if (group != NULL)
+ *group = CALLS[i].group;
+ return CALLS[i].call;
+ }
+ bail("unrecognized PAM call %s", name);
+}
+
+
+/*
+ * Given a PAM flag value without the leading PAM_, map it to the numeric
+ * value of that flag. Fails on any unrecognized string.
+ */
+static int
+string_to_flag(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(FLAGS); i++)
+ if (strcmp(name, FLAGS[i].name) == 0)
+ return FLAGS[i].value;
+ bail("unrecognized PAM flag %s", name);
+}
+
+
+/*
+ * Given a PAM group name, map it to the array index for the options array for
+ * that group. Fails on any unrecognized string.
+ */
+static enum group_type
+string_to_group(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(GROUPS); i++)
+ if (strcmp(name, GROUPS[i].name) == 0)
+ return GROUPS[i].group;
+ bail("unrecognized PAM group %s", name);
+}
+
+
+/*
+ * Given a syslog priority name, map it to the numeric value of that priority.
+ * Fails on any unrecognized string.
+ */
+static int
+string_to_priority(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(PRIORITIES); i++)
+ if (strcmp(name, PRIORITIES[i].name) == 0)
+ return PRIORITIES[i].priority;
+ bail("unrecognized syslog priority %s", name);
+}
+
+
+/*
+ * Given a PAM return status, map it to the actual expected value. Fails on
+ * any unrecognized string.
+ */
+static int
+string_to_status(const char *name)
+{
+ size_t i;
+
+ if (name == NULL)
+ bail("no PAM status on line");
+ for (i = 0; i < ARRAY_SIZE(RETURNS); i++)
+ if (strcmp(name, RETURNS[i].name) == 0)
+ return RETURNS[i].status;
+ bail("unrecognized PAM status %s", name);
+}
+
+
+/*
+ * Given a PAM prompt style value without the leading PAM_PROMPT_, map it to
+ * the numeric value of that flag. Fails on any unrecognized string.
+ */
+static int
+string_to_style(const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(STYLES); i++)
+ if (strcmp(name, STYLES[i].name) == 0)
+ return STYLES[i].style;
+ bail("unrecognized PAM prompt style %s", name);
+}
+
+
+/*
+ * We found a section delimiter while parsing another section. Rewind our
+ * input file back before the section delimiter so that we'll read it again.
+ * Takes the length of the line we read, which is used to determine how far to
+ * rewind.
+ */
+static void
+rewind_section(FILE *script, size_t length)
+{
+ if (fseek(script, -length - 1, SEEK_CUR) != 0)
+ sysbail("cannot rewind file");
+}
+
+
+/*
+ * Given a string that may contain %-escapes, expand it into the resulting
+ * value. The following escapes are supported:
+ *
+ * %i current UID (not target user UID)
+ * %n new password
+ * %p password
+ * %u username
+ * %0 user-supplied string
+ * ...
+ * %9 user-supplied string
+ *
+ * The %* escape is preserved as-is, as it has to be interpreted at the time
+ * of checking output. Returns the expanded string in newly-allocated memory.
+ */
+static char *
+expand_string(const char *template, const struct script_config *config)
+{
+ size_t length = 0;
+ const char *p, *extra;
+ char *output, *out;
+ char *uid = NULL;
+
+ length = 0;
+ for (p = template; *p != '\0'; p++) {
+ if (*p != '%')
+ length++;
+ else {
+ p++;
+ switch (*p) {
+ case 'i':
+ if (uid == NULL)
+ basprintf(&uid, "%lu", (unsigned long) getuid());
+ length += strlen(uid);
+ break;
+ case 'n':
+ if (config->newpass == NULL)
+ bail("new password not set");
+ length += strlen(config->newpass);
+ break;
+ case 'p':
+ if (config->password == NULL)
+ bail("password not set");
+ length += strlen(config->password);
+ break;
+ case 'u':
+ length += strlen(config->user);
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ if (config->extra[*p - '0'] == NULL)
+ bail("extra script parameter %%%c not set", *p);
+ length += strlen(config->extra[*p - '0']);
+ break;
+ case '*':
+ length += 2;
+ break;
+ default:
+ length++;
+ break;
+ }
+ }
+ }
+ output = bmalloc(length + 1);
+ for (p = template, out = output; *p != '\0'; p++) {
+ if (*p != '%')
+ *out++ = *p;
+ else {
+ p++;
+ switch (*p) {
+ case 'i':
+ assert(uid != NULL);
+ memcpy(out, uid, strlen(uid));
+ out += strlen(uid);
+ break;
+ case 'n':
+ memcpy(out, config->newpass, strlen(config->newpass));
+ out += strlen(config->newpass);
+ break;
+ case 'p':
+ memcpy(out, config->password, strlen(config->password));
+ out += strlen(config->password);
+ break;
+ case 'u':
+ memcpy(out, config->user, strlen(config->user));
+ out += strlen(config->user);
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ extra = config->extra[*p - '0'];
+ memcpy(out, extra, strlen(extra));
+ out += strlen(extra);
+ break;
+ case '*':
+ *out++ = '%';
+ *out++ = '*';
+ break;
+ default:
+ *out++ = *p;
+ break;
+ }
+ }
+ }
+ *out = '\0';
+ free(uid);
+ return output;
+}
+
+
+/*
+ * Given a whitespace-delimited string of PAM options, split it into an argv
+ * array and argc count and store it in the provided option struct.
+ */
+static void
+split_options(char *string, struct options *options,
+ const struct script_config *config)
+{
+ char *opt;
+ size_t size, count;
+
+ for (opt = strtok(string, " "); opt != NULL; opt = strtok(NULL, " ")) {
+ if (options->argv == NULL) {
+ options->argv = bcalloc(2, sizeof(const char *));
+ options->argv[0] = expand_string(opt, config);
+ options->argc = 1;
+ } else {
+ count = (options->argc + 2);
+ size = sizeof(const char *);
+ options->argv = breallocarray(options->argv, count, size);
+ options->argv[options->argc] = expand_string(opt, config);
+ options->argv[options->argc + 1] = NULL;
+ options->argc++;
+ }
+ }
+}
+
+
+/*
+ * Parse the options section of a PAM script. This consists of one or more
+ * lines in the format:
+ *
+ * <group> = <options>
+ *
+ * where options are either option names or option=value pairs, where the
+ * value may not contain whitespace. Returns an options struct, which stores
+ * argc and argv values for each group.
+ *
+ * Takes the work struct as an argument and puts values into its array.
+ */
+static void
+parse_options(FILE *script, struct work *work,
+ const struct script_config *config)
+{
+ char *line, *group, *token;
+ size_t length = 0;
+ enum group_type type;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ group = strtok(line, " ");
+ if (group == NULL)
+ bail("malformed script line");
+ if (group[0] == '[')
+ break;
+ type = string_to_group(group);
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed action line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed action line near %s", token);
+ token = strtok(NULL, "");
+ split_options(token, &work->options[type], config);
+ free(line);
+ }
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+}
+
+
+/*
+ * Parse the call portion of a PAM call in the run section of a PAM script.
+ * This handles parsing the PAM flags that optionally may be given as part of
+ * the call. Takes the token representing the call and a pointer to the
+ * action struct to fill in with the call and the option flags.
+ */
+static void
+parse_call(char *token, struct action *action)
+{
+ char *flags, *flag;
+
+ action->flags = 0;
+ flags = strchr(token, '(');
+ if (flags != NULL) {
+ *flags = '\0';
+ flags++;
+ for (flag = strtok(flags, "|,)"); flag != NULL;
+ flag = strtok(NULL, "|,)")) {
+ action->flags |= string_to_flag(flag);
+ }
+ }
+ action->call = string_to_call(token, &action->group);
+}
+
+
+/*
+ * Parse the run section of a PAM script. This consists of one or more lines
+ * in the format:
+ *
+ * <call> = <status>
+ *
+ * where <call> is a PAM call and <status> is what it should return. Returns
+ * a linked list of actions. Fails on any error in parsing.
+ */
+static struct action *
+parse_run(FILE *script)
+{
+ struct action *head = NULL, *current = NULL, *next;
+ char *line, *token, *call;
+ size_t length = 0;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ token = strtok(line, " ");
+ if (token[0] == '[')
+ break;
+ next = bmalloc(sizeof(struct action));
+ next->next = NULL;
+ if (head == NULL)
+ head = next;
+ else
+ current->next = next;
+ next->name = bstrdup(token);
+ call = token;
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed action line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed action line near %s", token);
+ token = strtok(NULL, " ");
+ next->status = string_to_status(token);
+ parse_call(call, next);
+ free(line);
+ current = next;
+ }
+ if (head == NULL)
+ bail("empty run section in script");
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+ return head;
+}
+
+
+/*
+ * Parse the end section of a PAM script. There is one supported line in the
+ * format:
+ *
+ * flags = <flag>|<flag>
+ *
+ * where <flag> is a flag to pass to pam_end. Returns the flags.
+ */
+static int
+parse_end(FILE *script)
+{
+ char *line, *token, *flag;
+ size_t length = 0;
+ int flags = PAM_SUCCESS;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ token = strtok(line, " ");
+ if (token[0] == '[')
+ break;
+ if (strcmp(token, "flags") != 0)
+ bail("unknown end setting %s", token);
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed end line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed end line near %s", token);
+ token = strtok(NULL, " ");
+ flag = strtok(token, "|");
+ while (flag != NULL) {
+ flags |= string_to_status(flag);
+ flag = strtok(NULL, "|");
+ }
+ free(line);
+ }
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+ return flags;
+}
+
+
+/*
+ * Parse the output section of a PAM script. This consists of zero or more
+ * lines in the format:
+ *
+ * PRIORITY some output information
+ * PRIORITY /output regex/
+ *
+ * where PRIORITY is replaced by the numeric syslog priority corresponding to
+ * that priority and the rest of the output undergoes %-esacape expansion.
+ * Returns the accumulated output as a vector.
+ */
+static struct output *
+parse_output(FILE *script, const struct script_config *config)
+{
+ char *line, *token, *message;
+ struct output *output;
+ int priority;
+
+ output = output_new();
+ if (output == NULL)
+ sysbail("cannot allocate vector");
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ token = strtok(line, " ");
+ priority = string_to_priority(token);
+ token = strtok(NULL, "");
+ if (token == NULL)
+ bail("malformed line %s", line);
+ message = expand_string(token, config);
+ output_add(output, priority, message);
+ free(message);
+ free(line);
+ }
+ return output;
+}
+
+
+/*
+ * Parse the prompts section of a PAM script. This consists of zero or more
+ * lines in one of the formats:
+ *
+ * type = prompt
+ * type = /prompt/
+ * type = prompt|response
+ * type = /prompt/|response
+ *
+ * If the type is error_msg or info, there is no response. Otherwise,
+ * everything after the last | is taken to be the response that should be
+ * provided to that prompt. The response undergoes %-escape expansion.
+ */
+static struct prompts *
+parse_prompts(FILE *script, const struct script_config *config)
+{
+ struct prompts *prompts = NULL;
+ struct prompt *prompt;
+ char *line, *token, *style, *end;
+ size_t size, count, i;
+ size_t length = 0;
+
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ length = strlen(line);
+ token = strtok(line, " ");
+ if (token[0] == '[')
+ break;
+ if (prompts == NULL) {
+ prompts = bcalloc(1, sizeof(struct prompts));
+ prompts->prompts = bcalloc(1, sizeof(struct prompt));
+ prompts->allocated = 1;
+ } else if (prompts->allocated == prompts->size) {
+ count = prompts->allocated * 2;
+ size = sizeof(struct prompt);
+ prompts->prompts = breallocarray(prompts->prompts, count, size);
+ prompts->allocated = count;
+ for (i = prompts->size; i < prompts->allocated; i++) {
+ prompts->prompts[i].prompt = NULL;
+ prompts->prompts[i].response = NULL;
+ }
+ }
+ prompt = &prompts->prompts[prompts->size];
+ style = token;
+ token = strtok(NULL, " ");
+ if (token == NULL)
+ bail("malformed prompt line");
+ if (strcmp(token, "=") != 0)
+ bail("malformed prompt line near %s", token);
+ prompt->style = string_to_style(style);
+ token = strtok(NULL, "");
+ if (prompt->style == PAM_ERROR_MSG || prompt->style == PAM_TEXT_INFO)
+ prompt->prompt = expand_string(token, config);
+ else {
+ end = strrchr(token, '|');
+ if (end == NULL)
+ bail("malformed prompt line near %s", token);
+ *end = '\0';
+ prompt->prompt = expand_string(token, config);
+ token = end + 1;
+ prompt->response = expand_string(token, config);
+ }
+ prompts->size++;
+ free(line);
+ }
+ if (line != NULL) {
+ free(line);
+ rewind_section(script, length);
+ }
+ return prompts;
+}
+
+
+/*
+ * Parse a PAM interaction script. This handles parsing of the top-level
+ * section markers and dispatches the parsing to other functions. Returns the
+ * total work to do as a work struct.
+ */
+struct work *
+parse_script(FILE *script, const struct script_config *config)
+{
+ struct work *work;
+ char *line, *token;
+
+ work = bmalloc(sizeof(struct work));
+ memset(work, 0, sizeof(struct work));
+ work->end_flags = PAM_SUCCESS;
+ for (line = readline(script); line != NULL; line = readline(script)) {
+ token = strtok(line, " ");
+ if (token[0] != '[')
+ bail("line outside of section: %s", line);
+ if (strcmp(token, "[options]") == 0)
+ parse_options(script, work, config);
+ else if (strcmp(token, "[run]") == 0)
+ work->actions = parse_run(script);
+ else if (strcmp(token, "[end]") == 0)
+ work->end_flags = parse_end(script);
+ else if (strcmp(token, "[output]") == 0)
+ work->output = parse_output(script, config);
+ else if (strcmp(token, "[prompts]") == 0)
+ work->prompts = parse_prompts(script, config);
+ else
+ bail("unknown section: %s", token);
+ free(line);
+ }
+ if (work->actions == NULL)
+ bail("no run section defined");
+ return work;
+}
diff --git a/tests/fakepam/data.c b/tests/fakepam/data.c
new file mode 100644
index 000000000000..0650d59e9b75
--- /dev/null
+++ b/tests/fakepam/data.c
@@ -0,0 +1,356 @@
+/*
+ * Data manipulation functions for the fake PAM library, used for testing.
+ *
+ * This file contains the implementation of pam_get_* and pam_set_* for the
+ * various data items supported by the PAM library, plus the PAM environment
+ * manipulation functions.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/pam.h>
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+
+/*
+ * Return a stored PAM data element in the provided data variable. As a
+ * special case, if the data is NULL, pretend it doesn't exist.
+ */
+int
+pam_get_data(const pam_handle_t *pamh, const char *name, const void **data)
+{
+ struct fakepam_data *item;
+
+ for (item = pamh->data; item != NULL; item = item->next)
+ if (strcmp(item->name, name) == 0) {
+ if (item->data == NULL)
+ return PAM_NO_MODULE_DATA;
+ *data = item->data;
+ return PAM_SUCCESS;
+ }
+ return PAM_NO_MODULE_DATA;
+}
+
+
+/*
+ * Store a data item. Replaces the existing data item (calling its cleanup)
+ * if it is already set; otherwise, add a new data item.
+ */
+int
+pam_set_data(pam_handle_t *pamh, const char *item, void *data,
+ void (*cleanup)(pam_handle_t *, void *, int))
+{
+ struct fakepam_data *p;
+
+ for (p = pamh->data; p != NULL; p = p->next)
+ if (strcmp(p->name, item) == 0) {
+ if (p->cleanup != NULL)
+ p->cleanup(pamh, p->data, PAM_DATA_REPLACE);
+ p->data = data;
+ p->cleanup = cleanup;
+ return PAM_SUCCESS;
+ }
+ p = malloc(sizeof(struct fakepam_data));
+ if (p == NULL)
+ return PAM_BUF_ERR;
+ p->name = strdup(item);
+ if (p->name == NULL) {
+ free(p);
+ return PAM_BUF_ERR;
+ }
+ p->data = data;
+ p->cleanup = cleanup;
+ p->next = pamh->data;
+ pamh->data = p;
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Retrieve a PAM item. Currently, this only supports a limited subset of the
+ * possible items.
+ */
+int
+pam_get_item(const pam_handle_t *pamh, int item, PAM_CONST void **data)
+{
+ switch (item) {
+ case PAM_AUTHTOK:
+ *data = pamh->authtok;
+ return PAM_SUCCESS;
+ case PAM_CONV:
+ if (pamh->conversation) {
+ *data = pamh->conversation;
+ return PAM_SUCCESS;
+ } else {
+ return PAM_BAD_ITEM;
+ }
+ case PAM_OLDAUTHTOK:
+ *data = pamh->oldauthtok;
+ return PAM_SUCCESS;
+ case PAM_RHOST:
+ *data = (PAM_CONST char *) pamh->rhost;
+ return PAM_SUCCESS;
+ case PAM_RUSER:
+ *data = (PAM_CONST char *) pamh->ruser;
+ return PAM_SUCCESS;
+ case PAM_SERVICE:
+ *data = (PAM_CONST char *) pamh->service;
+ return PAM_SUCCESS;
+ case PAM_TTY:
+ *data = (PAM_CONST char *) pamh->tty;
+ return PAM_SUCCESS;
+ case PAM_USER:
+ *data = (PAM_CONST char *) pamh->user;
+ return PAM_SUCCESS;
+ case PAM_USER_PROMPT:
+ *data = "login: ";
+ return PAM_SUCCESS;
+ default:
+ return PAM_BAD_ITEM;
+ }
+}
+
+
+/*
+ * Set a PAM item. Currently only PAM_USER is supported.
+ */
+int
+pam_set_item(pam_handle_t *pamh, int item, PAM_CONST void *data)
+{
+ switch (item) {
+ case PAM_AUTHTOK:
+ free(pamh->authtok);
+ pamh->authtok = strdup(data);
+ if (pamh->authtok == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_OLDAUTHTOK:
+ free(pamh->oldauthtok);
+ pamh->oldauthtok = strdup(data);
+ if (pamh->oldauthtok == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_RHOST:
+ free(pamh->rhost);
+ pamh->rhost = strdup(data);
+ if (pamh->rhost == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_RUSER:
+ free(pamh->ruser);
+ pamh->ruser = strdup(data);
+ if (pamh->ruser == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_TTY:
+ free(pamh->tty);
+ pamh->tty = strdup(data);
+ if (pamh->tty == NULL)
+ return PAM_BUF_ERR;
+ return PAM_SUCCESS;
+ case PAM_USER:
+ pamh->user = (const char *) data;
+ return PAM_SUCCESS;
+ default:
+ return PAM_BAD_ITEM;
+ }
+}
+
+
+/*
+ * Return the user for the PAM context.
+ */
+int
+pam_get_user(pam_handle_t *pamh, PAM_CONST char **user,
+ const char *prompt UNUSED)
+{
+ if (pamh->user == NULL)
+ return PAM_CONV_ERR;
+ else {
+ *user = (PAM_CONST char *) pamh->user;
+ return PAM_SUCCESS;
+ }
+}
+
+
+/*
+ * Return a setting in the PAM environment.
+ */
+PAM_CONST char *
+pam_getenv(pam_handle_t *pamh, const char *name)
+{
+ size_t i;
+
+ if (pamh->environ == NULL)
+ return NULL;
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ if (strncmp(name, pamh->environ[i], strlen(name)) == 0
+ && pamh->environ[i][strlen(name)] == '=')
+ return pamh->environ[i] + strlen(name) + 1;
+ return NULL;
+}
+
+
+/*
+ * Return a newly malloc'd copy of the complete PAM environment. This must be
+ * freed by the caller.
+ */
+char **
+pam_getenvlist(pam_handle_t *pamh)
+{
+ char **env;
+ size_t i;
+
+ if (pamh->environ == NULL) {
+ pamh->environ = malloc(sizeof(char *));
+ if (pamh->environ == NULL)
+ return NULL;
+ pamh->environ[0] = NULL;
+ }
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ ;
+ env = calloc(i + 1, sizeof(char *));
+ if (env == NULL)
+ return NULL;
+ for (i = 0; pamh->environ[i] != NULL; i++) {
+ env[i] = strdup(pamh->environ[i]);
+ if (env[i] == NULL)
+ goto fail;
+ }
+ env[i] = NULL;
+ return env;
+
+fail:
+ for (i = 0; env[i] != NULL; i++)
+ free(env[i]);
+ free(env);
+ return NULL;
+}
+
+
+/*
+ * Add a setting to the PAM environment. If there is another existing
+ * variable with the same value, the value is replaced, unless the setting
+ * doesn't end in an equal sign. If it doesn't end in an equal sign, any
+ * existing environment variable of that name is removed. This follows the
+ * Linux PAM semantics.
+ *
+ * On HP-UX, there is no separate PAM environment, so the module just uses the
+ * main environment. For our tests to work on that platform, we therefore
+ * have to do the same thing.
+ */
+#ifdef HAVE_PAM_GETENV
+int
+pam_putenv(pam_handle_t *pamh, const char *setting)
+{
+ char *copy = NULL;
+ const char *equals;
+ size_t namelen;
+ bool delete = false;
+ bool found = false;
+ size_t i, j;
+ char **env;
+
+ equals = strchr(setting, '=');
+ if (equals != NULL)
+ namelen = equals - setting;
+ else {
+ delete = true;
+ namelen = strlen(setting);
+ }
+ if (!delete) {
+ copy = strdup(setting);
+ if (copy == NULL)
+ return PAM_BUF_ERR;
+ }
+
+ /* Handle the first call to pam_putenv. */
+ if (pamh->environ == NULL) {
+ if (delete)
+ return PAM_BAD_ITEM;
+ pamh->environ = calloc(2, sizeof(char *));
+ if (pamh->environ == NULL) {
+ free(copy);
+ return PAM_BUF_ERR;
+ }
+ pamh->environ[0] = copy;
+ pamh->environ[1] = NULL;
+ return PAM_SUCCESS;
+ }
+
+ /*
+ * We have an existing array. See if we're replacing a value, deleting a
+ * value, or adding a new one. When deleting, waste a bit of memory but
+ * save some time by not bothering to reduce the size of the array.
+ */
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ if (strncmp(setting, pamh->environ[i], namelen) == 0
+ && pamh->environ[i][namelen] == '=') {
+ if (delete) {
+ free(pamh->environ[i]);
+ for (j = i + 1; pamh->environ[j] != NULL; i++, j++)
+ pamh->environ[i] = pamh->environ[j];
+ pamh->environ[i] = NULL;
+ } else {
+ free(pamh->environ[i]);
+ pamh->environ[i] = copy;
+ }
+ found = true;
+ break;
+ }
+ if (!found) {
+ if (delete)
+ return PAM_BAD_ITEM;
+ env = reallocarray(pamh->environ, (i + 2), sizeof(char *));
+ if (env == NULL) {
+ free(copy);
+ return PAM_BUF_ERR;
+ }
+ pamh->environ = env;
+ pamh->environ[i] = copy;
+ pamh->environ[i + 1] = NULL;
+ }
+ return PAM_SUCCESS;
+}
+
+#else /* !HAVE_PAM_GETENV */
+
+int
+pam_putenv(pam_handle_t *pamh UNUSED, const char *setting)
+{
+ return putenv((char *) setting);
+}
+
+#endif /* !HAVE_PAM_GETENV */
diff --git a/tests/fakepam/general.c b/tests/fakepam/general.c
new file mode 100644
index 000000000000..0f11bb2f7995
--- /dev/null
+++ b/tests/fakepam/general.c
@@ -0,0 +1,151 @@
+/*
+ * Interface for fake PAM library, used for testing.
+ *
+ * This contains the basic public interfaces for the fake PAM library, used
+ * for testing, and some general utility functions.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+
+/* Stores the static struct passwd returned by getpwnam if the name matches. */
+static struct passwd *pwd_info = NULL;
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+
+/*
+ * Initializes the pam_handle_t data structure. This function is only called
+ * from test programs, not from any of the module code. We can put anything
+ * we want in this structure, since it's opaque to the regular code.
+ */
+int
+pam_start(const char *service_name, const char *user,
+ const struct pam_conv *pam_conversation, pam_handle_t **pamh)
+{
+ struct pam_handle *handle;
+
+ handle = calloc(1, sizeof(struct pam_handle));
+ if (handle == NULL)
+ return PAM_BUF_ERR;
+ handle->service = service_name;
+ handle->user = user;
+ handle->conversation = pam_conversation;
+ *pamh = handle;
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Free the pam_handle_t data structure and related resources. This is
+ * important to test the data cleanups. Freeing the memory is not strictly
+ * required since it's only used for testing, but it helps keep our memory
+ * usage clean so that we can run the test suite under valgrind.
+ */
+int
+pam_end(pam_handle_t *pamh, int status)
+{
+ struct fakepam_data *item, *next;
+ size_t i;
+
+ if (pamh->environ != NULL) {
+ for (i = 0; pamh->environ[i] != NULL; i++)
+ free(pamh->environ[i]);
+ free(pamh->environ);
+ }
+ free(pamh->authtok);
+ free(pamh->oldauthtok);
+ free(pamh->rhost);
+ free(pamh->ruser);
+ free(pamh->tty);
+ for (item = pamh->data; item != NULL;) {
+ if (item->cleanup != NULL)
+ item->cleanup(pamh, item->data, status);
+ free(item->name);
+ next = item->next;
+ free(item);
+ item = next;
+ }
+ free(pamh);
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Interface specific to this fake PAM library to set the struct passwd that's
+ * returned by getpwnam queries if the name matches.
+ */
+void
+pam_set_pwd(struct passwd *pwd)
+{
+ pwd_info = pwd;
+}
+
+
+/*
+ * For testing purposes, we want to be able to intercept getpwnam. This is
+ * fairly easy on platforms that have pam_modutil_getpwnam, since then our
+ * code will always call that function and we can provide an implementation
+ * that does whatever we want. For platforms that don't have that function,
+ * we'll try to intercept the C library getpwnam function.
+ *
+ * We store only one struct passwd data structure statically. If the user
+ * we're looking up matches that, we return it; otherwise, we return NULL.
+ */
+#ifdef HAVE_PAM_MODUTIL_GETPWNAM
+struct passwd *
+pam_modutil_getpwnam(pam_handle_t *pamh UNUSED, const char *name)
+{
+ if (pwd_info != NULL && strcmp(pwd_info->pw_name, name) == 0)
+ return pwd_info;
+ else {
+ errno = 0;
+ return NULL;
+ }
+}
+#else
+struct passwd *
+getpwnam(const char *name)
+{
+ if (pwd_info != NULL && strcmp(pwd_info->pw_name, name) == 0)
+ return pwd_info;
+ else {
+ errno = 0;
+ return NULL;
+ }
+}
+#endif
diff --git a/tests/fakepam/internal.h b/tests/fakepam/internal.h
new file mode 100644
index 000000000000..3c6fedacd45e
--- /dev/null
+++ b/tests/fakepam/internal.h
@@ -0,0 +1,119 @@
+/*
+ * Internal data types and prototypes for the fake PAM test framework.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef FAKEPAM_INTERNAL_H
+#define FAKEPAM_INTERNAL_H 1
+
+#include <portable/pam.h>
+#include <sys/types.h>
+
+/* Forward declarations to avoid unnecessary includes. */
+struct output;
+struct script_config;
+
+/* The type of a PAM module call. */
+typedef int (*pam_call)(pam_handle_t *, int, int, const char **);
+
+/* The possible PAM groups as element numbers in an array of options. */
+enum group_type
+{
+ GROUP_ACCOUNT = 0,
+ GROUP_AUTH = 1,
+ GROUP_PASSWORD = 2,
+ GROUP_SESSION = 3,
+};
+
+/* Holds a PAM argc and argv. */
+struct options {
+ char **argv;
+ int argc;
+};
+
+/*
+ * Holds a linked list of actions: a PAM call that should return some
+ * status.
+ */
+struct action {
+ char *name;
+ pam_call call;
+ int flags;
+ enum group_type group;
+ int status;
+ struct action *next;
+};
+
+/* Holds an expected PAM prompt style, the prompt, and the response. */
+struct prompt {
+ int style;
+ char *prompt;
+ char *response;
+};
+
+/* Holds an array of PAM prompts and the current index into that array. */
+struct prompts {
+ struct prompt *prompts;
+ size_t size;
+ size_t allocated;
+ size_t current;
+};
+
+/*
+ * Holds the complete set of things that we should do, configuration for them,
+ * and expected output and return values.
+ */
+struct work {
+ struct options options[4];
+ struct action *actions;
+ struct prompts *prompts;
+ struct output *output;
+ int end_flags;
+};
+
+BEGIN_DECLS
+
+
+/* Create a new output struct. */
+struct output *output_new(void);
+
+/* Add a new output line (with numeric priority) to an output struct. */
+void output_add(struct output *, int, const char *);
+
+
+/*
+ * Parse a PAM interaction script. Returns the total work to do as a work
+ * struct.
+ */
+struct work *parse_script(FILE *, const struct script_config *);
+
+END_DECLS
+
+#endif /* !FAKEPAM_API_H */
diff --git a/tests/fakepam/kuserok.c b/tests/fakepam/kuserok.c
new file mode 100644
index 000000000000..d66bc1d03acc
--- /dev/null
+++ b/tests/fakepam/kuserok.c
@@ -0,0 +1,119 @@
+/*
+ * Replacement for krb5_kuserok for testing.
+ *
+ * This is a reimplementation of krb5_kuserok that uses the replacement
+ * getpwnam function and the special passwd struct internal to the fake PAM
+ * module to locate .k5login. The default Kerberos krb5_kuserok always calls
+ * the system getpwnam, which we may not be able to intercept, and will
+ * therefore fail because it can't locate the .k5login file for the test user
+ * (or succeed oddly because it finds some random file on the testing system).
+ *
+ * This implementation is drastically simplified from the Kerberos library
+ * version, and much less secure (which shouldn't matter since it's only
+ * acting on test data).
+ *
+ * This is an optional part of the fake PAM library and can be omitted when
+ * testing modules that don't use Kerberos.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/tap/string.h>
+
+
+/*
+ * Given a Kerberos principal representing the authenticated identity and the
+ * username of the local account, return true if that principal is authorized
+ * to log on to that account. The principal is authorized if the .k5login
+ * file does not exist and the user matches the localname form of the
+ * principal, or if the file does exist and the principal is listed in it.
+ *
+ * This version retrieves the home directory from the internal fake PAM
+ * library path.
+ */
+krb5_boolean
+krb5_kuserok(krb5_context ctx, krb5_principal princ, const char *user)
+{
+ char *principal, *path;
+ struct passwd *pwd;
+ FILE *file;
+ krb5_error_code code;
+ char buffer[BUFSIZ];
+ bool found = false;
+#ifdef HAVE_PAM_MODUTIL_GETPWNAM
+ struct pam_handle pamh;
+#endif
+
+ /*
+ * Find .k5login and confirm if it exists. If it doesn't, fall back on
+ * krb5_aname_to_localname.
+ */
+#ifdef HAVE_PAM_MODUTIL_GETPWNAM
+ memset(&pamh, 0, sizeof(pamh));
+ pwd = pam_modutil_getpwnam(&pamh, user);
+#else
+ pwd = getpwnam(user);
+#endif
+ if (pwd == NULL)
+ return false;
+ basprintf(&path, "%s/.k5login", pwd->pw_dir);
+ if (access(path, R_OK) < 0) {
+ free(path);
+ code = krb5_aname_to_localname(ctx, princ, sizeof(buffer), buffer);
+ return (code == 0 && strcmp(buffer, user) == 0);
+ }
+ file = fopen(path, "r");
+ if (file == NULL) {
+ free(path);
+ return false;
+ }
+ free(path);
+
+ /* .k5login exists. Scan it for the principal. */
+ if (krb5_unparse_name(ctx, princ, &principal) != 0) {
+ fclose(file);
+ return false;
+ }
+ while (!found && (fgets(buffer, sizeof(buffer), file) != NULL)) {
+ if (buffer[strlen(buffer) - 1] == '\n')
+ buffer[strlen(buffer) - 1] = '\0';
+ if (strcmp(buffer, principal) == 0)
+ found = true;
+ }
+ fclose(file);
+ krb5_free_unparsed_name(ctx, principal);
+ return found;
+}
diff --git a/tests/fakepam/logging.c b/tests/fakepam/logging.c
new file mode 100644
index 000000000000..c3a3fa044576
--- /dev/null
+++ b/tests/fakepam/logging.c
@@ -0,0 +1,183 @@
+/*
+ * Logging functions for the fake PAM library, used for testing.
+ *
+ * This file contains the implementation of pam_syslog and pam_vsyslog, which
+ * log to an internal buffer rather than to syslog, and the testing function
+ * used to recover that buffer. It also includes the pam_strerror
+ * implementation.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/internal.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/* The struct used to accumulate log messages. */
+static struct output *messages = NULL;
+
+
+/*
+ * Allocate a new, empty output struct and call bail if memory allocation
+ * fails.
+ */
+struct output *
+output_new(void)
+{
+ struct output *output;
+
+ output = bmalloc(sizeof(struct output));
+ output->count = 0;
+ output->allocated = 1;
+ output->lines = bmalloc(sizeof(output->lines[0]));
+ output->lines[0].line = NULL;
+ return output;
+}
+
+
+/*
+ * Add a new output line to the output struct, resizing the array as
+ * necessary. Calls bail if memory allocation fails.
+ */
+void
+output_add(struct output *output, int priority, const char *string)
+{
+ size_t next = output->count;
+ size_t size, n;
+
+ if (output->count == output->allocated) {
+ n = output->allocated + 1;
+ size = sizeof(output->lines[0]);
+ output->lines = breallocarray(output->lines, n, size);
+ output->allocated = n;
+ }
+ output->lines[next].priority = priority;
+ output->lines[next].line = bstrdup(string);
+ output->count++;
+}
+
+
+/*
+ * Return the error string associated with the PAM error code. We do this as
+ * a giant case statement so that we don't assume anything about the error
+ * codes used by the system PAM library.
+ */
+const char *
+pam_strerror(PAM_STRERROR_CONST pam_handle_t *pamh UNUSED, int code)
+{
+ /* clang-format off */
+ switch (code) {
+ case PAM_SUCCESS: return "No error";
+ case PAM_OPEN_ERR: return "Failure loading service module";
+ case PAM_SYMBOL_ERR: return "Symbol not found";
+ case PAM_SERVICE_ERR: return "Error in service module";
+ case PAM_SYSTEM_ERR: return "System error";
+ case PAM_BUF_ERR: return "Memory buffer error";
+ default: return "Unknown error";
+ }
+ /* clang-format on */
+}
+
+
+/*
+ * Log a message using variadic arguments. Just a wrapper around
+ * pam_vsyslog.
+ */
+void
+pam_syslog(const pam_handle_t *pamh, int priority, const char *format, ...)
+{
+ va_list args;
+
+ va_start(args, format);
+ pam_vsyslog(pamh, priority, format, args);
+ va_end(args);
+}
+
+
+/*
+ * Log a PAM error message with a given priority. Just appends the priority,
+ * a space, and the error message, followed by a newline, to the internal
+ * buffer, allocating new space if needed. Ignore memory allocation failures;
+ * we have no way of reporting them, but the tests will fail due to missing
+ * output.
+ */
+void
+pam_vsyslog(const pam_handle_t *pamh UNUSED, int priority, const char *format,
+ va_list args)
+{
+ char *message = NULL;
+
+ bvasprintf(&message, format, args);
+ if (messages == NULL)
+ messages = output_new();
+ output_add(messages, priority, message);
+ free(message);
+}
+
+
+/*
+ * Used by test code. Returns the accumulated messages in an output struct
+ * and starts a new one. Caller is responsible for freeing with
+ * pam_output_free.
+ */
+struct output *
+pam_output(void)
+{
+ struct output *output;
+
+ output = messages;
+ messages = NULL;
+ return output;
+}
+
+
+/*
+ * Free an output struct.
+ */
+void
+pam_output_free(struct output *output)
+{
+ size_t i;
+
+ if (output == NULL)
+ return;
+ for (i = 0; i < output->count; i++)
+ if (output->lines[i].line != NULL)
+ free(output->lines[i].line);
+ free(output->lines);
+ free(output);
+}
diff --git a/tests/fakepam/pam.h b/tests/fakepam/pam.h
new file mode 100644
index 000000000000..41f508e0f31f
--- /dev/null
+++ b/tests/fakepam/pam.h
@@ -0,0 +1,101 @@
+/*
+ * Testing interfaces to the fake PAM library.
+ *
+ * This header defines the interfaces to the fake PAM library that are used by
+ * test code to initialize the library and recover test data from it. We
+ * don't define any interface that we're going to duplicate from the main PAM
+ * API.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef FAKEPAM_PAM_H
+#define FAKEPAM_PAM_H 1
+
+#include <config.h>
+#include <portable/macros.h>
+#include <portable/pam.h>
+
+/* Used inside the fake PAM library to hold data items. */
+struct fakepam_data {
+ char *name;
+ void *data;
+ void (*cleanup)(pam_handle_t *, void *, int);
+ struct fakepam_data *next;
+};
+
+/* This is an opaque data structure, so we can put whatever we want in it. */
+struct pam_handle {
+ const char *service;
+ const char *user;
+ char *authtok;
+ char *oldauthtok;
+ char *rhost;
+ char *ruser;
+ char *tty;
+ const struct pam_conv *conversation;
+ char **environ;
+ struct fakepam_data *data;
+ struct passwd *pwd;
+};
+
+/*
+ * Used to accumulate output from the PAM module. Each call to a logging
+ * function will result in an additional line added to the array, and count
+ * will hold the total.
+ */
+struct output {
+ size_t count;
+ size_t allocated;
+ struct {
+ int priority;
+ char *line;
+ } * lines;
+};
+
+BEGIN_DECLS
+
+/*
+ * Sets the struct passwd returned by getpwnam calls. The last struct passed
+ * to this function will be returned provided the pw_name matches.
+ */
+void pam_set_pwd(struct passwd *pwd);
+
+/*
+ * Returns the accumulated messages logged with pam_syslog or pam_vsyslog
+ * since the last call to pam_output and then clears the output. Returns
+ * newly allocated memory that the caller is responsible for freeing with
+ * pam_output_free, or NULL if no output has been logged since the last call
+ * or since startup.
+ */
+struct output *pam_output(void);
+void pam_output_free(struct output *);
+
+END_DECLS
+
+#endif /* !FAKEPAM_API_H */
diff --git a/tests/fakepam/script.c b/tests/fakepam/script.c
new file mode 100644
index 000000000000..6f3812577960
--- /dev/null
+++ b/tests/fakepam/script.c
@@ -0,0 +1,411 @@
+/*
+ * Run a PAM interaction script for testing.
+ *
+ * Provides an interface that loads a PAM interaction script from a file and
+ * runs through that script, calling the internal PAM module functions and
+ * checking their results. This allows automation of PAM testing through
+ * external data files instead of coding everything in C.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2016, 2018, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#ifdef HAVE_REGCOMP
+# include <regex.h>
+#endif
+#include <syslog.h>
+
+#include <tests/fakepam/internal.h>
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/macros.h>
+#include <tests/tap/string.h>
+
+
+/*
+ * Compare a regex to a string. If regular expression support isn't
+ * available, we skip this test.
+ */
+#ifdef HAVE_REGCOMP
+static void __attribute__((__format__(printf, 3, 4)))
+like(const char *wanted, const char *seen, const char *format, ...)
+{
+ va_list args;
+ regex_t regex;
+ char err[BUFSIZ];
+ int status;
+
+ if (seen == NULL) {
+ fflush(stderr);
+ printf("# wanted: /%s/\n# seen: (null)\n", wanted);
+ va_start(args, format);
+ okv(0, format, args);
+ va_end(args);
+ return;
+ }
+ memset(&regex, 0, sizeof(regex));
+ status = regcomp(&regex, wanted, REG_EXTENDED | REG_NOSUB);
+ if (status != 0) {
+ regerror(status, &regex, err, sizeof(err));
+ bail("invalid regex /%s/: %s", wanted, err);
+ }
+ status = regexec(&regex, seen, 0, NULL, 0);
+ switch (status) {
+ case 0:
+ va_start(args, format);
+ okv(1, format, args);
+ va_end(args);
+ break;
+ case REG_NOMATCH:
+ printf("# wanted: /%s/\n# seen: %s\n", wanted, seen);
+ va_start(args, format);
+ okv(0, format, args);
+ va_end(args);
+ break;
+ default:
+ regerror(status, &regex, err, sizeof(err));
+ bail("regexec failed for regex /%s/: %s", wanted, err);
+ }
+ regfree(&regex);
+}
+#else /* !HAVE_REGCOMP */
+static void
+like(const char *wanted, const char *seen, const char *format UNUSED, ...)
+{
+ diag("wanted /%s/", wanted);
+ diag(" seen %s", seen);
+ skip("regex support not available");
+}
+#endif /* !HAVE_REGCOMP */
+
+
+/*
+ * Compare an expected string with a seen string, used by both output checking
+ * and prompt checking. This is a separate function because the expected
+ * string may be a regex, determined by seeing if it starts and ends with a
+ * slash (/), which may require a regex comparison.
+ *
+ * Eventually calls either is_string or ok to report results via TAP.
+ */
+static void __attribute__((__format__(printf, 3, 4)))
+compare_string(char *wanted, char *seen, const char *format, ...)
+{
+ va_list args;
+ char *comment, *regex;
+ size_t length;
+
+ /* Format the comment since we need it regardless. */
+ va_start(args, format);
+ bvasprintf(&comment, format, args);
+ va_end(args);
+
+ /* Check whether the wanted string is a regex. */
+ length = strlen(wanted);
+ if (wanted[0] == '/' && wanted[length - 1] == '/') {
+ regex = bstrndup(wanted + 1, length - 2);
+ like(regex, seen, "%s", comment);
+ free(regex);
+ } else {
+ is_string(wanted, seen, "%s", comment);
+ }
+ free(comment);
+}
+
+
+/*
+ * The PAM conversation function. Takes the prompts struct from the
+ * configuration and interacts appropriately. If a prompt is of the expected
+ * type but not the expected string, it still responds; if it's not of the
+ * expected type, it returns PAM_CONV_ERR.
+ *
+ * Currently only handles a single prompt at a time.
+ */
+static int
+converse(int num_msg, const struct pam_message **msg,
+ struct pam_response **resp, void *appdata_ptr)
+{
+ struct prompts *prompts = appdata_ptr;
+ struct prompt *prompt;
+ char *message;
+ size_t length;
+ int i;
+
+ *resp = bcalloc(num_msg, sizeof(struct pam_response));
+ for (i = 0; i < num_msg; i++) {
+ message = bstrdup(msg[i]->msg);
+
+ /* Remove newlines for comparison purposes. */
+ length = strlen(message);
+ while (length > 0 && message[length - 1] == '\n')
+ message[length-- - 1] = '\0';
+
+ /* Check if we've gotten too many prompts but quietly ignore them. */
+ if (prompts->current >= prompts->size) {
+ diag("unexpected prompt: %s", message);
+ free(message);
+ ok(0, "more prompts than expected");
+ continue;
+ }
+
+ /* Be sure everything matches and return the response, if any. */
+ prompt = &prompts->prompts[prompts->current];
+ is_int(prompt->style, msg[i]->msg_style, "style of prompt %lu",
+ (unsigned long) prompts->current + 1);
+ compare_string(prompt->prompt, message, "value of prompt %lu",
+ (unsigned long) prompts->current + 1);
+ free(message);
+ prompts->current++;
+ if (prompt->style == msg[i]->msg_style && prompt->response != NULL) {
+ (*resp)[i].resp = bstrdup(prompt->response);
+ (*resp)[i].resp_retcode = 0;
+ }
+ }
+
+ /*
+ * Always return success even if the prompts don't match. Otherwise,
+ * we're likely to abort the conversation in the middle and possibly
+ * leave passwords set incorrectly.
+ */
+ return PAM_SUCCESS;
+}
+
+
+/*
+ * Check the actual PAM output against the expected output. We divide the
+ * expected and seen output into separate lines and compare each one so that
+ * we can handle regular expressions and the output priority.
+ */
+static void
+check_output(const struct output *wanted, const struct output *seen)
+{
+ size_t i;
+
+ if (wanted == NULL && seen == NULL)
+ ok(1, "no output");
+ else if (wanted == NULL) {
+ for (i = 0; i < seen->count; i++)
+ diag("unexpected: (%d) %s", seen->lines[i].priority,
+ seen->lines[i].line);
+ ok(0, "no output");
+ } else if (seen == NULL) {
+ for (i = 0; i < wanted->count; i++) {
+ is_int(wanted->lines[i].priority, 0, "output priority %lu",
+ (unsigned long) i + 1);
+ is_string(wanted->lines[i].line, NULL, "output line %lu",
+ (unsigned long) i + 1);
+ }
+ } else {
+ for (i = 0; i < wanted->count && i < seen->count; i++) {
+ is_int(wanted->lines[i].priority, seen->lines[i].priority,
+ "output priority %lu", (unsigned long) i + 1);
+ compare_string(wanted->lines[i].line, seen->lines[i].line,
+ "output line %lu", (unsigned long) i + 1);
+ }
+ if (wanted->count > seen->count)
+ for (i = seen->count; i < wanted->count; i++) {
+ is_int(wanted->lines[i].priority, 0, "output priority %lu",
+ (unsigned long) i + 1);
+ is_string(wanted->lines[i].line, NULL, "output line %lu",
+ (unsigned long) i + 1);
+ }
+ if (seen->count > wanted->count) {
+ for (i = wanted->count; i < seen->count; i++)
+ diag("unexpected: (%d) %s", seen->lines[i].priority,
+ seen->lines[i].line);
+ ok(0, "unexpected output lines");
+ } else {
+ ok(1, "no excess output");
+ }
+ }
+}
+
+
+/*
+ * The core of the work. Given the path to a PAM interaction script, which
+ * may be relative to C_TAP_SOURCE or C_TAP_BUILD, the user (may be NULL), and
+ * the stored password (may be NULL), run that script, outputting the results
+ * in TAP format.
+ */
+void
+run_script(const char *file, const struct script_config *config)
+{
+ char *path;
+ struct output *output;
+ FILE *script;
+ struct work *work;
+ struct options *opts;
+ struct action *action, *oaction;
+ struct pam_conv conv = {NULL, NULL};
+ pam_handle_t *pamh;
+ int status;
+ size_t i, j;
+ const char *argv_empty[] = {NULL};
+
+ /* Open and parse the script. */
+ if (access(file, R_OK) == 0)
+ path = bstrdup(file);
+ else {
+ path = test_file_path(file);
+ if (path == NULL)
+ bail("cannot find PAM script %s", file);
+ }
+ script = fopen(path, "r");
+ if (script == NULL)
+ sysbail("cannot open %s", path);
+ work = parse_script(script, config);
+ fclose(script);
+ diag("Starting %s", file);
+ if (work->prompts != NULL) {
+ conv.conv = converse;
+ conv.appdata_ptr = work->prompts;
+ }
+
+ /* Initialize PAM. */
+ status = pam_start("test", config->user, &conv, &pamh);
+ if (status != PAM_SUCCESS)
+ sysbail("cannot create PAM handle");
+ if (config->authtok != NULL)
+ pamh->authtok = bstrdup(config->authtok);
+ if (config->oldauthtok != NULL)
+ pamh->oldauthtok = bstrdup(config->oldauthtok);
+
+ /* Run the actions and check their return status. */
+ for (action = work->actions; action != NULL; action = action->next) {
+ if (work->options[action->group].argv == NULL)
+ status = (*action->call)(pamh, action->flags, 0, argv_empty);
+ else {
+ opts = &work->options[action->group];
+ status = (*action->call)(pamh, action->flags, opts->argc,
+ (const char **) opts->argv);
+ }
+ is_int(action->status, status, "status for %s", action->name);
+ }
+ output = pam_output();
+ check_output(work->output, output);
+ pam_output_free(output);
+
+ /* If we have a test callback, call it now. */
+ if (config->callback != NULL)
+ config->callback(pamh, config, config->data);
+
+ /* Free memory and return. */
+ pam_end(pamh, work->end_flags);
+ action = work->actions;
+ while (action != NULL) {
+ free(action->name);
+ oaction = action;
+ action = action->next;
+ free(oaction);
+ }
+ for (i = 0; i < ARRAY_SIZE(work->options); i++)
+ if (work->options[i].argv != NULL) {
+ for (j = 0; work->options[i].argv[j] != NULL; j++)
+ free(work->options[i].argv[j]);
+ free(work->options[i].argv);
+ }
+ if (work->output)
+ pam_output_free(work->output);
+ if (work->prompts != NULL) {
+ for (i = 0; i < work->prompts->size; i++) {
+ free(work->prompts->prompts[i].prompt);
+ free(work->prompts->prompts[i].response);
+ }
+ free(work->prompts->prompts);
+ free(work->prompts);
+ }
+ free(work);
+ free(path);
+}
+
+
+/*
+ * Check a filename for acceptable characters. Returns true if the file
+ * consists solely of [a-zA-Z0-9-] and false otherwise.
+ */
+static bool
+valid_filename(const char *filename)
+{
+ const char *p;
+
+ for (p = filename; *p != '\0'; p++) {
+ if (*p >= 'A' && *p <= 'Z')
+ continue;
+ if (*p >= 'a' && *p <= 'z')
+ continue;
+ if (*p >= '0' && *p <= '9')
+ continue;
+ if (*p == '-')
+ continue;
+ return false;
+ }
+ return true;
+}
+
+
+/*
+ * The same as run_script, but run every script found in the given directory,
+ * skipping file names that contain characters other than alphanumerics and -.
+ */
+void
+run_script_dir(const char *dir, const struct script_config *config)
+{
+ DIR *handle;
+ struct dirent *entry;
+ const char *path;
+ char *file;
+
+ if (access(dir, R_OK) == 0)
+ path = dir;
+ else
+ path = test_file_path(dir);
+ handle = opendir(path);
+ if (handle == NULL)
+ sysbail("cannot open directory %s", dir);
+ errno = 0;
+ while ((entry = readdir(handle)) != NULL) {
+ if (!valid_filename(entry->d_name))
+ continue;
+ basprintf(&file, "%s/%s", path, entry->d_name);
+ run_script(file, config);
+ free(file);
+ errno = 0;
+ }
+ if (errno != 0)
+ sysbail("cannot read directory %s", dir);
+ closedir(handle);
+ if (path != dir)
+ test_file_path_free((char *) path);
+}
diff --git a/tests/fakepam/script.h b/tests/fakepam/script.h
new file mode 100644
index 000000000000..c99fc12f55d2
--- /dev/null
+++ b/tests/fakepam/script.h
@@ -0,0 +1,82 @@
+/*
+ * PAM interaction script API.
+ *
+ * Provides an interface that loads a PAM interaction script from a file and
+ * runs through that script, calling the internal PAM module functions and
+ * checking their results. This allows automation of PAM testing through
+ * external data files instead of coding everything in C.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2016 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TESTS_MODULE_SCRIPT_H
+#define TESTS_MODULE_SCRIPT_H 1
+
+#include <portable/pam.h>
+
+#include <tests/tap/basic.h>
+
+/* A test callback called after PAM functions are run but before pam_end. */
+struct script_config;
+typedef void (*script_callback)(pam_handle_t *, const struct script_config *,
+ void *);
+
+/* Configuration for the PAM interaction script API. */
+struct script_config {
+ const char *user; /* Username to pass into pam_start (%u). */
+ const char *password; /* Substituted for %p in prompts. */
+ const char *newpass; /* Substituted for %n in prompts. */
+ const char *extra[10]; /* Substituted for %0-%9 in logging. */
+ const char *authtok; /* Stored as AUTHTOK before PAM. */
+ const char *oldauthtok; /* Stored as OLDAUTHTOK before PAM. */
+ script_callback callback; /* Called after PAM, before pam_end. */
+ void *data; /* Passed to the callback function. */
+};
+
+BEGIN_DECLS
+
+/*
+ * Given the file name of an interaction script (which may be a full path or
+ * relative to C_TAP_SOURCE or C_TAP_BUILD) and configuration containing other
+ * parameters such as the user, run that script, reporting the results via the
+ * TAP format.
+ */
+void run_script(const char *file, const struct script_config *)
+ __attribute__((__nonnull__));
+
+/*
+ * The same as run_script, but run every script found in the given directory,
+ * skipping file names that contain characters other than alphanumerics and -.
+ */
+void run_script_dir(const char *dir, const struct script_config *)
+ __attribute__((__nonnull__));
+
+END_DECLS
+
+#endif /* !TESTS_MODULE_SCRIPT_H */
diff --git a/tests/module/alt-auth-t.c b/tests/module/alt-auth-t.c
new file mode 100644
index 000000000000..df32ff941001
--- /dev/null
+++ b/tests/module/alt-auth-t.c
@@ -0,0 +1,117 @@
+/*
+ * Tests for the alt_auth_map functionality in libpam-krb5.
+ *
+ * This test case tests the variations of the alt_auth_map functionality for
+ * both authentication and account management. It requires a Kerberos
+ * configuration, but does not attempt to save a session ticket cache (to
+ * avoid requiring user configuration).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *user;
+
+ /*
+ * Load the Kerberos principal and password from a file, but set the
+ * principal as extra[0] and use something else bogus as the user. We
+ * want to test that alt_auth_map works when there's no relationship
+ * between the mapped principal and the user.
+ */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = "bogus-nonexistent-account";
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->username;
+ config.extra[1] = krbconf->userprinc;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+ config.extra[2] = "bogus.example.com";
+
+ /* Test without password prompting. */
+ plan_lazy();
+ run_script("data/scripts/alt-auth/basic", &config);
+ run_script("data/scripts/alt-auth/basic-debug", &config);
+ run_script("data/scripts/alt-auth/fail", &config);
+ run_script("data/scripts/alt-auth/fail-debug", &config);
+ run_script("data/scripts/alt-auth/force", &config);
+ run_script("data/scripts/alt-auth/only", &config);
+
+ /*
+ * If the alternate account exists but the password is incorrect, we
+ * should not fall back to the regular account. Test with debug so that
+ * we don't need two principals configured.
+ */
+ config.authtok = "bogus incorrect password";
+ run_script("data/scripts/alt-auth/force-fail-debug", &config);
+
+ /*
+ * Switch to our correct user (but wrong realm) realm to test username
+ * mapping to a different realm.
+ */
+ config.authtok = krbconf->password;
+ config.user = krbconf->username;
+ config.extra[2] = krbconf->realm;
+ run_script("data/scripts/alt-auth/username-map", &config);
+
+ /*
+ * Split the username into two parts, one in the PAM configuration and one
+ * in the real username, so that we can test interpolation of the username
+ * when %s isn't the first token.
+ */
+ config.user = &krbconf->username[1];
+ user = bstrndup(krbconf->username, 1);
+ config.extra[3] = user;
+ run_script("data/scripts/alt-auth/username-map-prefix", &config);
+ free(user);
+ config.extra[3] = NULL;
+
+ /*
+ * Ensure that we don't add the realm of the authentication username when
+ * the alt_auth_map already includes a realm.
+ */
+ basprintf(&user, "%s@foo.example.com", krbconf->username);
+ config.user = user;
+ diag("re-running username-map with fully-qualified PAM user");
+ run_script("data/scripts/alt-auth/username-map", &config);
+ free(user);
+
+ /*
+ * Add the password and make the user match our authentication principal,
+ * and then test fallback to normal authentication when alternative
+ * authentication fails.
+ */
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+ config.extra[2] = krbconf->realm;
+ run_script("data/scripts/alt-auth/fallback", &config);
+ run_script("data/scripts/alt-auth/fallback-debug", &config);
+ run_script("data/scripts/alt-auth/fallback-realm", &config);
+ run_script("data/scripts/alt-auth/force-fallback", &config);
+ run_script("data/scripts/alt-auth/only-fail", &config);
+
+ return 0;
+}
diff --git a/tests/module/bad-authtok-t.c b/tests/module/bad-authtok-t.c
new file mode 100644
index 000000000000..385dd5946849
--- /dev/null
+++ b/tests/module/bad-authtok-t.c
@@ -0,0 +1,53 @@
+/*
+ * Authentication tests for the pam-krb5 module with an incorrect AUTHTOK.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available and that run with an incorrect AUTHTOK
+ * already set. They test various prompting fallback cases. They don't write
+ * a ticket cache (which requires additional work to test the cache
+ * ownership).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+
+ /* Set the authtok to something bogus. */
+ config.authtok = "BAD PASSWORD THAT WILL NOT WORK";
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ plan_lazy();
+ run_script_dir("data/scripts/bad-authtok", &config);
+
+ return 0;
+}
diff --git a/tests/module/basic-t.c b/tests/module/basic-t.c
new file mode 100644
index 000000000000..cacad5906ffb
--- /dev/null
+++ b/tests/module/basic-t.c
@@ -0,0 +1,67 @@
+/*
+ * Basic tests for the pam-krb5 module.
+ *
+ * This test case includes all tests that can be done without having Kerberos
+ * configured and a username and password available, and without any special
+ * configuration.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct passwd pwd;
+ char *uid;
+ char *uidplus;
+
+ plan_lazy();
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that this test will run on any system.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ /* Create a fake passwd struct for our user. */
+ memset(&pwd, 0, sizeof(pwd));
+ pwd.pw_name = (char *) "root";
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ pam_set_pwd(&pwd);
+
+ /*
+ * Attempt login as the root user to test ignore_root. Set our current
+ * UID and a UID one larger for testing minimum_uid.
+ */
+ basprintf(&uid, "%lu", (unsigned long) pwd.pw_uid);
+ basprintf(&uidplus, "%lu", (unsigned long) pwd.pw_uid + 1);
+ memset(&config, 0, sizeof(config));
+ config.user = "root";
+ config.extra[0] = uid;
+ config.extra[1] = uidplus;
+
+ run_script_dir("data/scripts/basic", &config);
+
+ free(uid);
+ free(uidplus);
+ return 0;
+}
diff --git a/tests/module/cache-cleanup-t.c b/tests/module/cache-cleanup-t.c
new file mode 100644
index 000000000000..8b5012fc3507
--- /dev/null
+++ b/tests/module/cache-cleanup-t.c
@@ -0,0 +1,104 @@
+/*
+ * Test for properly cleaning up ticket caches.
+ *
+ * Verify that the temporary Kerberos ticket cache generated during
+ * authentication is cleaned up on pam_end, even if no session was opened.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <dirent.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ DIR *tmpdir;
+ struct dirent *file;
+ char *tmppath, *path;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Get the temporary directory and store that as the %1 substitution. */
+ tmppath = test_tmpdir();
+ config.extra[1] = tmppath;
+
+ plan_lazy();
+
+ /*
+ * We need to ensure that the only thing in the test temporary directory
+ * is the krb5.conf file that we generated and any valgrind logs, since
+ * we're going to check for cleanup by looking for any out-of-place files.
+ */
+ tmpdir = opendir(tmppath);
+ if (tmpdir == NULL)
+ sysbail("cannot open directory %s", tmppath);
+ while ((file = readdir(tmpdir)) != NULL) {
+ if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0)
+ continue;
+ if (strcmp(file->d_name, "krb5.conf") == 0)
+ continue;
+ if (strcmp(file->d_name, "valgrind") == 0)
+ continue;
+ basprintf(&path, "%s/%s", tmppath, file->d_name);
+ if (unlink(path) < 0)
+ sysbail("cannot delete temporary file %s", path);
+ free(path);
+ }
+ closedir(tmpdir);
+
+ /*
+ * Authenticate only, call pam_end, and be sure the ticket cache is
+ * gone. The auth-only script sets ccache_dir to the temporary directory,
+ * so the module will create a temporary ticket cache there and then
+ * should clean it up.
+ */
+ run_script("data/scripts/cache-cleanup/auth-only", &config);
+ path = NULL;
+ tmpdir = opendir(tmppath);
+ if (tmpdir == NULL)
+ sysbail("cannot open directory %s", tmppath);
+ while ((file = readdir(tmpdir)) != NULL) {
+ if (strcmp(file->d_name, ".") == 0 || strcmp(file->d_name, "..") == 0)
+ continue;
+ if (strcmp(file->d_name, "krb5.conf") == 0)
+ continue;
+ if (strcmp(file->d_name, "valgrind") == 0)
+ continue;
+ if (path == NULL)
+ basprintf(&path, "%s/%s", tmppath, file->d_name);
+ }
+ closedir(tmpdir);
+ if (path != NULL)
+ diag("found stray temporary file %s", path);
+ ok(path == NULL, "ticket cache cleaned up");
+ if (path != NULL)
+ free(path);
+
+ test_tmpdir_free(tmppath);
+ return 0;
+}
diff --git a/tests/module/cache-t.c b/tests/module/cache-t.c
new file mode 100644
index 000000000000..8ec82df7c460
--- /dev/null
+++ b/tests/module/cache-t.c
@@ -0,0 +1,210 @@
+/*
+ * Authentication tests for the pam-krb5 module with ticket cache.
+ *
+ * This test case includes all tests that require Kerberos to be configured, a
+ * username and password available, and a ticket cache created, but with the
+ * PAM module running as the same user for which the ticket cache will be
+ * created (so without setuid and with chown doing nothing).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+/* Additional data used by the cache check callback. */
+struct extra {
+ char *realm;
+ char *cache_path;
+};
+
+
+/*
+ * PAM test callback to check whether we created a ticket cache and the ticket
+ * cache is for the correct user.
+ */
+static void
+check_cache(const char *file, const struct script_config *config,
+ const struct extra *extra)
+{
+ struct stat st;
+ krb5_error_code code;
+ krb5_context ctx = NULL;
+ krb5_ccache ccache = NULL;
+ krb5_principal princ = NULL;
+ krb5_principal tgtprinc = NULL;
+ krb5_creds in, out;
+ char *principal = NULL;
+
+ /* Check ownership and permissions. */
+ is_int(0, stat(file, &st), "cache exists");
+ is_int(getuid(), st.st_uid, "...with correct UID");
+ is_int(getgid(), st.st_gid, "...with correct GID");
+ is_int(0600, (st.st_mode & 0777), "...with correct permissions");
+
+ /* Check the existence of the ticket cache and its principal. */
+ code = krb5_init_context(&ctx);
+ if (code != 0)
+ bail("cannot create Kerberos context");
+ code = krb5_cc_resolve(ctx, file, &ccache);
+ is_int(0, code, "able to resolve Kerberos ticket cache");
+ code = krb5_cc_get_principal(ctx, ccache, &princ);
+ is_int(0, code, "able to get principal");
+ code = krb5_unparse_name(ctx, princ, &principal);
+ is_int(0, code, "...and principal is valid");
+ is_string(config->extra[0], principal, "...and matches our principal");
+
+ /* Retrieve the krbtgt for the realm and check properties. */
+ code = krb5_build_principal_ext(
+ ctx, &tgtprinc, (unsigned int) strlen(extra->realm), extra->realm,
+ KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME, strlen(extra->realm), extra->realm,
+ NULL);
+ if (code != 0)
+ bail("cannot create krbtgt principal name");
+ memset(&in, 0, sizeof(in));
+ memset(&out, 0, sizeof(out));
+ in.server = tgtprinc;
+ in.client = princ;
+ code = krb5_cc_retrieve_cred(ctx, ccache, KRB5_TC_MATCH_SRV_NAMEONLY, &in,
+ &out);
+ is_int(0, code, "able to get krbtgt credentials");
+ ok(out.times.endtime > time(NULL) + 30 * 60, "...good for 30 minutes");
+ krb5_free_cred_contents(ctx, &out);
+
+ /* Close things and release memory. */
+ krb5_free_principal(ctx, tgtprinc);
+ krb5_free_unparsed_name(ctx, principal);
+ krb5_free_principal(ctx, princ);
+ krb5_cc_close(ctx, ccache);
+ krb5_free_context(ctx);
+}
+
+
+/*
+ * Same as check_cache except unlink the ticket cache afterwards. Used to
+ * check the ticket cache in cases where the PAM module will not clean it up
+ * afterwards, such as calling pam_end with PAM_DATA_SILENT.
+ */
+static void
+check_cache_callback(pam_handle_t *pamh, const struct script_config *config,
+ void *data)
+{
+ struct extra *extra = data;
+ const char *cache, *file;
+ char *prefix;
+
+ cache = pam_getenv(pamh, "KRB5CCNAME");
+ ok(cache != NULL, "KRB5CCNAME is set in PAM environment");
+ if (cache == NULL)
+ return;
+ basprintf(&prefix, "FILE:/tmp/krb5cc_%lu_", (unsigned long) getuid());
+ diag("KRB5CCNAME = %s", cache);
+ ok(strncmp(prefix, cache, strlen(prefix)) == 0,
+ "cache file name prefix is correct");
+ free(prefix);
+ file = cache + strlen("FILE:");
+ extra->cache_path = bstrdup(file);
+ check_cache(file, config, extra);
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *k5login;
+ struct extra extra;
+ struct passwd pwd;
+ FILE *file;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ extra.realm = krbconf->realm;
+ extra.cache_path = NULL;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Create a fake passwd struct for our user. */
+ memset(&pwd, 0, sizeof(pwd));
+ pwd.pw_name = krbconf->username;
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ basprintf(&pwd.pw_dir, "%s/tmp", getenv("BUILD"));
+ pam_set_pwd(&pwd);
+
+ plan_lazy();
+
+ /* Basic test. */
+ run_script("data/scripts/cache/basic", &config);
+
+ /* Check the cache status before the session is closed. */
+ config.callback = check_cache_callback;
+ config.data = &extra;
+ run_script("data/scripts/cache/open-session", &config);
+ free(extra.cache_path);
+ extra.cache_path = NULL;
+
+ /*
+ * Try again but passing PAM_DATA_SILENT to pam_end. This should leave
+ * the ticket cache intact.
+ */
+ run_script("data/scripts/cache/end-data-silent", &config);
+ check_cache(extra.cache_path, &config, &extra);
+ if (unlink(extra.cache_path) < 0)
+ sysdiag("unable to unlink temporary cache %s", extra.cache_path);
+ free(extra.cache_path);
+ extra.cache_path = NULL;
+
+ /* Change the authenticating user and test search_k5login. */
+ pwd.pw_name = (char *) "testuser";
+ pam_set_pwd(&pwd);
+ config.user = "testuser";
+ basprintf(&k5login, "%s/.k5login", pwd.pw_dir);
+ file = fopen(k5login, "w");
+ if (file == NULL)
+ sysbail("cannot create %s", k5login);
+ if (fprintf(file, "%s\n", krbconf->userprinc) < 0)
+ sysbail("cannot write to %s", k5login);
+ if (fclose(file) < 0)
+ sysbail("cannot flush %s", k5login);
+ run_script("data/scripts/cache/search-k5login", &config);
+ free(extra.cache_path);
+ extra.cache_path = NULL;
+ config.callback = NULL;
+ run_script("data/scripts/cache/search-k5login-debug", &config);
+ unlink(k5login);
+ free(k5login);
+
+ /* Test search_k5login when no .k5login file exists. */
+ pwd.pw_name = krbconf->username;
+ pam_set_pwd(&pwd);
+ config.user = krbconf->username;
+ diag("testing search_k5login with no .k5login file");
+ run_script("data/scripts/cache/search-k5login", &config);
+
+ free(pwd.pw_dir);
+ return 0;
+}
diff --git a/tests/module/expired-t.c b/tests/module/expired-t.c
new file mode 100644
index 000000000000..01a1892a0d04
--- /dev/null
+++ b/tests/module/expired-t.c
@@ -0,0 +1,175 @@
+/*
+ * Tests for the pam-krb5 module with an expired password.
+ *
+ * This test case checks correct handling of an account whose password has
+ * expired and the multiple different paths the module can take for handling
+ * that case.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+#include <time.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kadmin.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *newpass, *date;
+ struct passwd pwd;
+ time_t now;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.password = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /*
+ * Ensure we can expire the password. Heimdal has a prompt for the
+ * expiration time, so save that to use as a substitution in the script.
+ */
+ now = time(NULL) - 1;
+ if (!kerberos_expire_password(krbconf->userprinc, now))
+ skip_all("kadmin not configured or kadmin mismatch");
+ date = bstrdup(ctime(&now));
+ date[strlen(date) - 1] = '\0';
+ config.extra[1] = date;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Create a fake passwd struct for our user. */
+ memset(&pwd, 0, sizeof(pwd));
+ pwd.pw_name = krbconf->username;
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ basprintf(&pwd.pw_dir, "%s/tmp", getenv("BUILD"));
+ pam_set_pwd(&pwd);
+
+ /*
+ * We'll be changing the password to something new. This needs to be
+ * sufficiently random that it's unlikely to fall afoul of password
+ * strength checking.
+ */
+ basprintf(&newpass, "ngh1,a%lu nn9af6", (unsigned long) getpid());
+ config.newpass = newpass;
+
+ plan_lazy();
+
+ /*
+ * Default behavior. We have to distinguish between two versions of
+ * Heimdal for testing because the prompts changed substantially. Use the
+ * existence of krb5_principal_set_comp_string to distinguish because it
+ * was introduced at the same time.
+ */
+#ifdef HAVE_KRB5_HEIMDAL
+# ifdef HAVE_KRB5_PRINCIPAL_SET_COMP_STRING
+ run_script("data/scripts/expired/basic-heimdal", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-debug", &config);
+# else
+ run_script("data/scripts/expired/basic-heimdal-old", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-old-debug", &config);
+# endif
+#else
+ run_script("data/scripts/expired/basic-mit", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-mit-debug", &config);
+#endif
+
+ /* Test again with PAM_SILENT, specified two ways. */
+#ifdef HAVE_KRB5_HEIMDAL
+ config.newpass = newpass;
+ config.password = krbconf->password;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-silent", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-heimdal-flag-silent", &config);
+#else
+ config.newpass = newpass;
+ config.password = krbconf->password;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-mit-silent", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/basic-mit-flag-silent", &config);
+#endif
+
+ /*
+ * We can only run the remaining checks if we can suppress the Kerberos
+ * library behavior of prompting for a new password when the password has
+ * expired.
+ */
+#ifdef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT
+
+ /* Check the forced failure behavior. */
+ run_script("data/scripts/expired/fail", &config);
+ run_script("data/scripts/expired/fail-debug", &config);
+
+ /*
+ * Defer the error to the account management check.
+ *
+ * Skip this check on Heimdal currently (Heimdal 7.4.0) because its
+ * implementation of krb5_get_init_creds_opt_set_change_password_prompt is
+ * incomplete. See <https://github.com/heimdal/heimdal/issues/322>.
+ */
+# ifdef HAVE_KRB5_HEIMDAL
+ skip_block(2, "deferring password changes broken in Heimdal");
+# else
+ config.newpass = newpass;
+ config.password = krbconf->password;
+ config.authtok = krbconf->password;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/defer-mit", &config);
+ config.newpass = krbconf->password;
+ config.password = newpass;
+ config.authtok = newpass;
+ kerberos_expire_password(krbconf->userprinc, now);
+ run_script("data/scripts/expired/defer-mit-debug", &config);
+# endif
+
+#else /* !HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT */
+
+ /* Mention that we skipped something for the record. */
+ skip_block(4, "cannot disable library password prompting");
+
+#endif /* HAVE_KRB5_GET_INIT_CREDS_OPT_SET_CHANGE_PASSWORD_PROMPT */
+
+ /* In case we ran into some error, try to unexpire the password. */
+ kerberos_expire_password(krbconf->userprinc, 0);
+
+ free(date);
+ free(newpass);
+ free(pwd.pw_dir);
+ return 0;
+}
diff --git a/tests/module/fast-anon-t.c b/tests/module/fast-anon-t.c
new file mode 100644
index 000000000000..6355a5154f69
--- /dev/null
+++ b/tests/module/fast-anon-t.c
@@ -0,0 +1,108 @@
+/*
+ * Tests for anonymous FAST support in pam-krb5.
+ *
+ * Tests for anonymous Flexible Authentication Secure Tunneling, a mechanism
+ * for improving the preauthentication part of the Kerberos protocol and
+ * protecting it against various attacks.
+ *
+ * This is broken out from the other FAST tests because it uses PKINIT, and
+ * PKINIT code cannot be tested under valgrind with MIT Kerberos due to some
+ * bug in valgrind.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+
+
+/*
+ * Test whether anonymous authentication works. If this doesn't, we need to
+ * skip the tests of anonymous FAST.
+ */
+static bool
+anon_fast_works(void)
+{
+ krb5_context ctx;
+ krb5_error_code retval;
+ krb5_principal princ = NULL;
+ char *realm;
+ krb5_creds creds;
+ krb5_get_init_creds_opt *opts = NULL;
+
+ /* Construct the anonymous principal name. */
+ retval = krb5_init_context(&ctx);
+ if (retval != 0)
+ bail("cannot initialize Kerberos");
+ retval = krb5_get_default_realm(ctx, &realm);
+ if (retval != 0)
+ bail("cannot get default realm");
+ retval = krb5_build_principal_ext(
+ ctx, &princ, (unsigned int) strlen(realm), realm,
+ strlen(KRB5_WELLKNOWN_NAME), KRB5_WELLKNOWN_NAME,
+ strlen(KRB5_ANON_NAME), KRB5_ANON_NAME, NULL);
+ if (retval != 0)
+ bail("cannot construct anonymous principal");
+ krb5_free_default_realm(ctx, realm);
+
+ /* Obtain the credentials. */
+ memset(&creds, 0, sizeof(creds));
+ retval = krb5_get_init_creds_opt_alloc(ctx, &opts);
+ if (retval != 0)
+ bail("cannot create credential options");
+ krb5_get_init_creds_opt_set_anonymous(opts, 1);
+ krb5_get_init_creds_opt_set_tkt_life(opts, 60);
+ retval = krb5_get_init_creds_password(ctx, &creds, princ, NULL, NULL, NULL,
+ 0, NULL, opts);
+
+ /* Clean up. */
+ if (princ != NULL)
+ krb5_free_principal(ctx, princ);
+ if (opts != NULL)
+ krb5_get_init_creds_opt_free(ctx, opts);
+ krb5_free_cred_contents(ctx, &creds);
+
+ /* Return whether authentication succeeded. */
+ return (retval == 0);
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Skip the test if FAST is not available. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+ skip_all("FAST support not available");
+#endif
+
+ /* Initialize Kerberos configuration. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Skip the test if anonymous PKINIT doesn't work. */
+ if (!anon_fast_works())
+ skip_all("anonymous PKINIT failed");
+
+ /* Test anonymous FAST. */
+ plan_lazy();
+ run_script("data/scripts/fast/anonymous", &config);
+ run_script("data/scripts/fast/anonymous-debug", &config);
+
+ return 0;
+}
diff --git a/tests/module/fast-t.c b/tests/module/fast-t.c
new file mode 100644
index 000000000000..51fee27098c8
--- /dev/null
+++ b/tests/module/fast-t.c
@@ -0,0 +1,57 @@
+/*
+ * Tests for authenticated FAST support in pam-krb5.
+ *
+ * Tests for Flexible Authentication Secure Tunneling, a mechanism for
+ * improving the preauthentication part of the Kerberos protocol and
+ * protecting it against various attacks. This tests authenticated FAST;
+ * anonymous FAST is tested separately.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Skip the test if FAST is not available. */
+#ifndef HAVE_KRB5_GET_INIT_CREDS_OPT_SET_FAST_CCACHE_NAME
+ skip_all("FAST support not available");
+#endif
+
+ /* Initialize Kerberos configuration. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_BOTH);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.authtok = krbconf->password;
+ config.extra[0] = krbconf->cache;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ /* Test fast_ccache */
+ plan_lazy();
+ run_script("data/scripts/fast/ccache", &config);
+ run_script("data/scripts/fast/ccache-debug", &config);
+ run_script("data/scripts/fast/no-ccache", &config);
+ run_script("data/scripts/fast/no-ccache-debug", &config);
+
+ return 0;
+}
diff --git a/tests/module/long-t.c b/tests/module/long-t.c
new file mode 100644
index 000000000000..73614b0f6ec9
--- /dev/null
+++ b/tests/module/long-t.c
@@ -0,0 +1,46 @@
+/*
+ * Excessively long password tests for the pam-krb5 module.
+ *
+ * This test case includes all tests for excessively long passwords that can
+ * be done without having Kerberos configured and a username and password
+ * available.
+ *
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ char *password;
+
+ plan_lazy();
+
+ memset(&config, 0, sizeof(config));
+ config.user = "test";
+
+ /* Test a password that is too long. */
+ password = bcalloc_type(PAM_MAX_RESP_SIZE + 1, char);
+ memset(password, 'a', PAM_MAX_RESP_SIZE);
+ config.password = password;
+ run_script("data/scripts/long/password", &config);
+ run_script("data/scripts/long/password-debug", &config);
+
+ /* Test a stored authtok that's too long. */
+ config.authtok = password;
+ config.password = "testing";
+ run_script("data/scripts/long/use-first", &config);
+ run_script("data/scripts/long/use-first-debug", &config);
+
+ free(password);
+ return 0;
+}
diff --git a/tests/module/no-cache-t.c b/tests/module/no-cache-t.c
new file mode 100644
index 000000000000..8b282d1de397
--- /dev/null
+++ b/tests/module/no-cache-t.c
@@ -0,0 +1,47 @@
+/*
+ * Authentication tests for the pam-krb5 module without a ticket cache.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available, but which don't write a ticket cache
+ * (which requires additional work to test the cache ownership). This test
+ * does not set AUTHTOK.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ plan_lazy();
+ run_script_dir("data/scripts/no-cache", &config);
+
+ return 0;
+}
diff --git a/tests/module/pam-user-t.c b/tests/module/pam-user-t.c
new file mode 100644
index 000000000000..72cc21eebae3
--- /dev/null
+++ b/tests/module/pam-user-t.c
@@ -0,0 +1,80 @@
+/*
+ * Tests for PAM_USER handling.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available, but which don't write a ticket cache
+ * (which requires additional work to test the cache ownership).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014, 2020 Russ Allbery <eagle@eyrie.org>
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/macros.h>
+
+
+/*
+ * Callback to check that PAM_USER matches the desired value, passed in as the
+ * data parameter.
+ */
+static void
+check_pam_user(pam_handle_t *pamh, const struct script_config *config UNUSED,
+ void *data)
+{
+ int retval;
+ const char *name = NULL;
+ const char *expected = data;
+
+ retval = pam_get_item(pamh, PAM_USER, (PAM_CONST void **) &name);
+ is_int(PAM_SUCCESS, retval, "Found PAM_USER");
+ is_string(expected, name, "...matching %s", expected);
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.password = krbconf->password;
+ config.callback = check_pam_user;
+ config.extra[0] = krbconf->username;
+ config.extra[1] = krbconf->userprinc;
+
+ /*
+ * Generate a testing krb5.conf file matching the realm of the Kerberos
+ * configuration so that canonicalization will work.
+ */
+ kerberos_generate_conf(krbconf->realm);
+
+ /* Declare our plan. */
+ plan_lazy();
+
+ /* Authentication without a realm. No canonicalization. */
+ config.user = krbconf->username;
+ config.data = krbconf->username;
+ run_script("data/scripts/pam-user/update", &config);
+
+ /* Authentication with the local realm. Should be canonicalized. */
+ config.user = krbconf->userprinc;
+ run_script("data/scripts/pam-user/update", &config);
+
+ /*
+ * Now, test again with user updates disabled. The PAM_USER value should
+ * now not be canonicalized.
+ */
+ config.data = krbconf->userprinc;
+ run_script("data/scripts/pam-user/no-update", &config);
+
+ return 0;
+}
diff --git a/tests/module/password-t.c b/tests/module/password-t.c
new file mode 100644
index 000000000000..bdf9762bc6cb
--- /dev/null
+++ b/tests/module/password-t.c
@@ -0,0 +1,152 @@
+/*
+ * Authentication tests for the pam-krb5 module with ticket cache.
+ *
+ * This test case includes all tests that require Kerberos to be configured, a
+ * username and password available, and a ticket cache created, but with the
+ * PAM module running as the same user for which the ticket cache will be
+ * created (so without setuid and with chown doing nothing).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/macros.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+static void
+check_authtok(pam_handle_t *pamh, const struct script_config *config,
+ void *data UNUSED)
+{
+ int retval;
+ const char *authtok;
+
+ retval = pam_get_item(pamh, PAM_AUTHTOK, (PAM_CONST void **) &authtok);
+ is_int(PAM_SUCCESS, retval, "Found PAM_AUTHTOK");
+ is_string(config->newpass, authtok, "...and it is correct");
+}
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ char *newpass;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.password = krbconf->password;
+ config.extra[0] = krbconf->userprinc;
+
+ /* Generate a testing krb5.conf file. */
+ kerberos_generate_conf(krbconf->realm);
+
+ plan_lazy();
+
+ /*
+ * First test trying to change the password to something that's
+ * excessively long.
+ */
+ newpass = bcalloc_type(PAM_MAX_RESP_SIZE + 1, char);
+ memset(newpass, 'a', PAM_MAX_RESP_SIZE);
+ config.newpass = newpass;
+ run_script("data/scripts/password/too-long", &config);
+ run_script("data/scripts/password/too-long-debug", &config);
+
+ /* Test use_authtok with an excessively long password. */
+ config.newpass = NULL;
+ config.authtok = newpass;
+ run_script("data/scripts/password/authtok-too-long", &config);
+ run_script("data/scripts/password/authtok-too-long-debug", &config);
+
+ /*
+ * Change the password to something new. This needs to be sufficiently
+ * random that it's unlikely to fall afoul of password strength checking.
+ */
+ free(newpass);
+ config.authtok = NULL;
+ basprintf(&newpass, "ngh1,a%lu nn9af6%lu", (unsigned long) getpid(),
+ (unsigned long) time(NULL));
+ config.newpass = newpass;
+ run_script("data/scripts/password/basic", &config);
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/basic-debug", &config);
+
+ /* Test prompt_principal with password change. */
+ config.password = krbconf->password;
+ config.newpass = newpass;
+ run_script("data/scripts/password/prompt-principal", &config);
+
+ /* Change the password back and test expose-account. */
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/expose", &config);
+
+ /*
+ * Test two banner settings by changing the password and then changing it
+ * back again.
+ */
+ config.password = krbconf->password;
+ config.newpass = newpass;
+ run_script("data/scripts/password/banner", &config);
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/no-banner", &config);
+
+ /* Do the same, but with expose_account set as well. */
+ config.password = krbconf->password;
+ config.newpass = newpass;
+ run_script("data/scripts/password/banner-expose", &config);
+ config.password = newpass;
+ config.newpass = krbconf->password;
+ run_script("data/scripts/password/no-banner-expose", &config);
+
+ /* Test use_authtok. */
+ config.password = krbconf->password;
+ config.newpass = NULL;
+ config.authtok = newpass;
+ run_script("data/scripts/password/authtok", &config);
+
+ /* Test use_authtok with force_first_pass. */
+ config.password = NULL;
+ config.authtok = krbconf->password;
+ config.oldauthtok = newpass;
+ run_script("data/scripts/password/authtok-force", &config);
+
+ /*
+ * Ensure PAM_AUTHTOK and PAM_OLDAUTHTOK are set even if the user is
+ * ignored.
+ */
+ config.user = "root";
+ config.authtok = NULL;
+ config.oldauthtok = NULL;
+ config.password = "old-password";
+ config.newpass = "new-password";
+ config.callback = check_authtok;
+ run_script("data/scripts/password/ignore", &config);
+
+ free(newpass);
+ return 0;
+}
diff --git a/tests/module/pkinit-t.c b/tests/module/pkinit-t.c
new file mode 100644
index 000000000000..6bbb6993b2af
--- /dev/null
+++ b/tests/module/pkinit-t.c
@@ -0,0 +1,98 @@
+/*
+ * PKINIT authentication tests for the pam-krb5 module.
+ *
+ * This test case includes tests that require a PKINIT certificate, but which
+ * don't write a Kerberos ticket cache.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+#if defined(HAVE_KRB5_MIT) && defined(PATH_OPENSSL)
+ const char **generate_pkcs12;
+ char *tmpdir, *pkcs12_path;
+#endif
+
+ /* Load the Kerberos principal and certificate path. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PKINIT);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->pkinit_principal;
+ config.extra[0] = krbconf->pkinit_cert;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ /* Check things that are the same with both Kerberos implementations. */
+ plan_lazy();
+ run_script("data/scripts/pkinit/basic", &config);
+ run_script("data/scripts/pkinit/basic-debug", &config);
+ run_script("data/scripts/pkinit/prompt-use", &config);
+ run_script("data/scripts/pkinit/prompt-try", &config);
+ run_script("data/scripts/pkinit/try-pkinit", &config);
+
+ /* Debugging output is a little different between the implementations. */
+#ifdef HAVE_KRB5_HEIMDAL
+ run_script("data/scripts/pkinit/try-pkinit-debug", &config);
+#else
+ run_script("data/scripts/pkinit/try-pkinit-debug-mit", &config);
+#endif
+
+ /* Only MIT Kerberos supports setting preauth options. */
+#ifdef HAVE_KRB5_MIT
+ run_script("data/scripts/pkinit/preauth-opt-mit", &config);
+#endif
+
+ /*
+ * If OpenSSL is available, test prompting with MIT Kerberos since we have
+ * to implement the prompting for the use_pkinit case ourselves. To do
+ * this, convert the input PKINIT certificate to a PKCS12 file with a
+ * password.
+ */
+#if defined(HAVE_KRB5_MIT) && defined(PATH_OPENSSL)
+ tmpdir = test_tmpdir();
+ basprintf(&pkcs12_path, "%s/%s", tmpdir, "pkinit-pkcs12");
+ generate_pkcs12 = bcalloc_type(10, const char *);
+ generate_pkcs12[0] = PATH_OPENSSL;
+ generate_pkcs12[1] = "pkcs12";
+ generate_pkcs12[2] = "-export";
+ generate_pkcs12[3] = "-in";
+ generate_pkcs12[4] = krbconf->pkinit_cert;
+ generate_pkcs12[5] = "-password";
+ generate_pkcs12[6] = "pass:some-password";
+ generate_pkcs12[7] = "-out";
+ generate_pkcs12[8] = pkcs12_path;
+ generate_pkcs12[9] = NULL;
+ run_setup(generate_pkcs12);
+ free(generate_pkcs12);
+ config.extra[0] = pkcs12_path;
+ config.extra[1] = "some-password";
+ run_script("data/scripts/pkinit/pin-mit", &config);
+ unlink(pkcs12_path);
+ free(pkcs12_path);
+ test_tmpdir_free(tmpdir);
+#endif /* HAVE_KRB5_MIT && PATH_OPENSSL */
+
+ return 0;
+}
diff --git a/tests/module/realm-t.c b/tests/module/realm-t.c
new file mode 100644
index 000000000000..d5643ca1f3e5
--- /dev/null
+++ b/tests/module/realm-t.c
@@ -0,0 +1,87 @@
+/*
+ * Authentication tests for realm support in pam-krb5.
+ *
+ * Test the realm and user_realm option in the PAM configuration, which is
+ * special in several ways since it influences krb5.conf parsing and is read
+ * out of order in the initial configuration.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#include <pwd.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+ struct passwd pwd;
+ FILE *file;
+ char *k5login;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->username;
+ config.authtok = krbconf->password;
+
+ /* Don't keep track of the tests in each script. */
+ plan_lazy();
+
+ /* Start with a nonexistent default realm for authentication failure. */
+ kerberos_generate_conf("bogus.example.com");
+ config.extra[0] = "bogus.example.com";
+ run_script("data/scripts/realm/fail-no-realm", &config);
+ run_script("data/scripts/realm/fail-no-realm-debug", &config);
+
+ /* Running a script that sets realm properly should pass. */
+ config.extra[0] = krbconf->realm;
+ run_script("data/scripts/realm/pass-realm", &config);
+
+ /* Setting user_realm should continue to fail due to no .k5login file. */
+ run_script("data/scripts/realm/fail-user-realm", &config);
+
+ /* If we add a .k5login file for the user, user_realm should work. */
+ pwd.pw_name = krbconf->username;
+ pwd.pw_uid = getuid();
+ pwd.pw_gid = getgid();
+ pwd.pw_dir = test_tmpdir();
+ pam_set_pwd(&pwd);
+ basprintf(&k5login, "%s/.k5login", pwd.pw_dir);
+ file = fopen(k5login, "w");
+ if (file == NULL)
+ sysbail("cannot create %s", k5login);
+ if (fprintf(file, "%s\n", krbconf->userprinc) < 0)
+ sysbail("cannot write to %s", k5login);
+ if (fclose(file) < 0)
+ sysbail("cannot flush %s", k5login);
+ run_script("data/scripts/realm/pass-user-realm", &config);
+ pam_set_pwd(NULL);
+ unlink(k5login);
+ free(k5login);
+ test_tmpdir_free(pwd.pw_dir);
+
+ /* Switch to the correct realm, but set the wrong realm in PAM. */
+ kerberos_generate_conf(krbconf->realm);
+ config.extra[0] = "bogus.example.com";
+ run_script("data/scripts/realm/fail-realm", &config);
+ run_script("data/scripts/realm/fail-bad-user-realm", &config);
+
+ return 0;
+}
diff --git a/tests/module/stacked-t.c b/tests/module/stacked-t.c
new file mode 100644
index 000000000000..ef8e70885ecb
--- /dev/null
+++ b/tests/module/stacked-t.c
@@ -0,0 +1,50 @@
+/*
+ * Authentication tests for the pam-krb5 module with an existing AUTHTOK.
+ *
+ * This test case includes tests that require Kerberos to be configured and a
+ * username and password available and that run with AUTHTOK already set, but
+ * which don't write a ticket cache (which requires additional work to test
+ * the cache ownership).
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ struct kerberos_config *krbconf;
+
+ /* Load the Kerberos principal and password from a file. */
+ krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
+ memset(&config, 0, sizeof(config));
+ config.user = krbconf->userprinc;
+ config.password = krbconf->password;
+ config.authtok = krbconf->password;
+
+ /*
+ * Generate a testing krb5.conf file with a nonexistent default realm so
+ * that we can be sure that our principals will stay fully-qualified in
+ * the logs.
+ */
+ kerberos_generate_conf("bogus.example.com");
+
+ plan_lazy();
+ run_script_dir("data/scripts/stacked", &config);
+
+ return 0;
+}
diff --git a/tests/module/trace-t.c b/tests/module/trace-t.c
new file mode 100644
index 000000000000..db3aa67f9e24
--- /dev/null
+++ b/tests/module/trace-t.c
@@ -0,0 +1,48 @@
+/*
+ * Tests for trace logging in the pam-krb5 module.
+ *
+ * Checks that trace logging is handled properly. This is currently very
+ * simple and just checks that the file is created.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * SPDX-License-Identifier: BSD-3-clause or GPL-1+
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/script.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct script_config config;
+ char *tmpdir, *trace;
+
+ plan_lazy();
+
+ memset(&config, 0, sizeof(config));
+ config.user = "testuser";
+ tmpdir = test_tmpdir();
+ basprintf(&trace, "%s/trace", tmpdir);
+ config.extra[0] = trace;
+#ifdef HAVE_KRB5_SET_TRACE_FILENAME
+ run_script("data/scripts/trace/supported", &config);
+ is_int(0, access(trace, F_OK), "Trace file was created");
+ unlink(trace);
+#else
+ run_script("data/scripts/trace/unsupported", &config);
+ is_int(-1, access(trace, F_OK), "Trace file does not exist");
+#endif
+
+ free(trace);
+ test_tmpdir_free(tmpdir);
+ return 0;
+}
diff --git a/tests/pam-util/args-t.c b/tests/pam-util/args-t.c
new file mode 100644
index 000000000000..4ec102e511ed
--- /dev/null
+++ b/tests/pam-util/args-t.c
@@ -0,0 +1,86 @@
+/*
+ * PAM utility argument initialization test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2012-2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <pam-util/args.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_conv conv = {NULL, NULL};
+ struct pam_args *args;
+
+ plan(12);
+
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("Fake PAM initialization failed");
+ args = putil_args_new(pamh, 0);
+ ok(args != NULL, "New args struct is not NULL");
+ if (args == NULL)
+ ok_block(7, 0, "...args struct is NULL");
+ else {
+ ok(args->pamh == pamh, "...and pamh is correct");
+ ok(args->config == NULL, "...and config is NULL");
+ ok(args->user == NULL, "...and user is NULL");
+ is_int(args->debug, false, "...and debug is false");
+ is_int(args->silent, false, "...and silent is false");
+#ifdef HAVE_KRB5
+ ok(args->ctx != NULL, "...and the Kerberos context is initialized");
+ ok(args->realm == NULL, "...and realm is NULL");
+#else
+ skip_block(2, "Kerberos support not configured");
+#endif
+ }
+ putil_args_free(args);
+ ok(1, "Freeing the args struct works");
+
+ args = putil_args_new(pamh, PAM_SILENT);
+ ok(args != NULL, "New args struct with PAM_SILENT is not NULL");
+ if (args == NULL)
+ ok(0, "...args is NULL");
+ else
+ is_int(args->silent, true, "...and silent is true");
+ putil_args_free(args);
+
+ putil_args_free(NULL);
+ ok(1, "Freeing a NULL args struct works");
+
+ pam_end(pamh, 0);
+
+ return 0;
+}
diff --git a/tests/pam-util/fakepam-t.c b/tests/pam-util/fakepam-t.c
new file mode 100644
index 000000000000..1e09c5fdde75
--- /dev/null
+++ b/tests/pam-util/fakepam-t.c
@@ -0,0 +1,121 @@
+/*
+ * Fake PAM library test suite.
+ *
+ * This is not actually a test for the pam-util layer, but rather is a test
+ * for the trickier components of the fake PAM library that in turn is used to
+ * test the pam-util layer and PAM modules.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_conv conv = {NULL, NULL};
+ char **env;
+ size_t i;
+
+ /*
+ * Skip this test if the native PAM library doesn't support a PAM
+ * environment, since we "break" pam_putenv to mirror the native behavior
+ * in that case.
+ */
+#ifndef HAVE_PAM_GETENV
+ skip_all("system doesn't support PAM environment");
+#endif
+
+ plan(33);
+
+ /* Basic environment manipulation. */
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("Fake PAM initialization failed");
+ is_int(PAM_BAD_ITEM, pam_putenv(pamh, "TEST"), "delete when NULL");
+ ok(pam_getenv(pamh, "TEST") == NULL, "getenv when NULL");
+ env = pam_getenvlist(pamh);
+ ok(env != NULL, "getenvlist when NULL returns non-NULL");
+ if (env == NULL)
+ bail("pam_getenvlist returned NULL");
+ is_string(NULL, env[0], "...but first element is NULL");
+ for (i = 0; env[i] != NULL; i++)
+ free(env[i]);
+ free(env);
+
+ /* putenv and getenv. */
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "TEST=foo"), "putenv TEST");
+ is_string("foo", pam_getenv(pamh, "TEST"), "getenv TEST");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=bar"), "putenv FOO");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "BAR=baz"), "putenv BAR");
+ is_string("foo", pam_getenv(pamh, "TEST"), "getenv TEST");
+ is_string("bar", pam_getenv(pamh, "FOO"), "getenv FOO");
+ is_string("baz", pam_getenv(pamh, "BAR"), "getenv BAR");
+ ok(pam_getenv(pamh, "BAZ") == NULL, "getenv BAZ is NULL");
+
+ /* Replacing and deleting environment variables. */
+ is_int(PAM_BAD_ITEM, pam_putenv(pamh, "BAZ"), "putenv nonexistent delete");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=foo"), "putenv replace");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOON=bar=n"), "putenv prefix");
+ is_string("foo", pam_getenv(pamh, "FOO"), "getenv FOO");
+ is_string("bar=n", pam_getenv(pamh, "FOON"), "getenv FOON");
+ is_int(PAM_BAD_ITEM, pam_putenv(pamh, "FO"), "putenv delete FO");
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO"), "putenv delete FOO");
+ ok(pam_getenv(pamh, "FOO") == NULL, "getenv FOO is NULL");
+ is_string("bar=n", pam_getenv(pamh, "FOON"), "getenv FOON");
+ is_string("baz", pam_getenv(pamh, "BAR"), "getenv BAR");
+
+ /* pam_getenvlist. */
+ env = pam_getenvlist(pamh);
+ ok(env != NULL, "getenvlist not NULL");
+ if (env == NULL)
+ bail("pam_getenvlist returned NULL");
+ is_string("TEST=foo", env[0], "getenvlist TEST");
+ is_string("BAR=baz", env[1], "getenvlist BAR");
+ is_string("FOON=bar=n", env[2], "getenvlist FOON");
+ ok(env[3] == NULL, "getenvlist length");
+ for (i = 0; env[i] != NULL; i++)
+ free(env[i]);
+ free(env);
+ is_int(PAM_SUCCESS, pam_putenv(pamh, "FOO=foo"), "putenv FOO");
+ is_string("TEST=foo", pamh->environ[0], "pamh environ TEST");
+ is_string("BAR=baz", pamh->environ[1], "pamh environ BAR");
+ is_string("FOON=bar=n", pamh->environ[2], "pamh environ FOON");
+ is_string("FOO=foo", pamh->environ[3], "pamh environ FOO");
+ ok(pamh->environ[4] == NULL, "pamh environ length");
+
+ pam_end(pamh, 0);
+
+ return 0;
+}
diff --git a/tests/pam-util/logging-t.c b/tests/pam-util/logging-t.c
new file mode 100644
index 000000000000..84072bd6b91a
--- /dev/null
+++ b/tests/pam-util/logging-t.c
@@ -0,0 +1,146 @@
+/*
+ * PAM logging test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <syslog.h>
+
+#include <pam-util/args.h>
+#include <pam-util/logging.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* Test a normal PAM logging function. */
+#define TEST(func, p, n) \
+ do { \
+ (func)(args, "%s", "foo"); \
+ seen = pam_output(); \
+ is_int((p), seen->lines[0].priority, "priority %d", (p)); \
+ is_string("foo", seen->lines[0].line, "line %s", (n)); \
+ pam_output_free(seen); \
+ } while (0)
+
+/* Test a PAM error logging function. */
+#define TEST_PAM(func, c, p, n) \
+ do { \
+ (func)(args, (c), "%s", "bar"); \
+ if ((c) == PAM_SUCCESS) \
+ expected = strdup("bar"); \
+ else \
+ basprintf(&expected, "%s: %s", "bar", \
+ pam_strerror(args->pamh, c)); \
+ seen = pam_output(); \
+ is_int((p), seen->lines[0].priority, "priority %s", (n)); \
+ is_string(expected, seen->lines[0].line, "line %s", (n)); \
+ pam_output_free(seen); \
+ free(expected); \
+ } while (0)
+
+/* Test a PAM Kerberos error logging function .*/
+#define TEST_KRB5(func, p, n) \
+ do { \
+ const char *msg; \
+ \
+ code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ); \
+ (func)(args, code, "%s", "krb"); \
+ code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ); \
+ msg = krb5_get_error_message(args->ctx, code); \
+ basprintf(&expected, "%s: %s", "krb", msg); \
+ seen = pam_output(); \
+ is_int((p), seen->lines[0].priority, "priority %s", (n)); \
+ is_string(expected, seen->lines[0].line, "line %s", (n)); \
+ pam_output_free(seen); \
+ free(expected); \
+ krb5_free_error_message(args->ctx, msg); \
+ } while (0)
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_args *args;
+ struct pam_conv conv = {NULL, NULL};
+ char *expected;
+ struct output *seen;
+#ifdef HAVE_KRB5
+ krb5_error_code code;
+ krb5_principal princ;
+#endif
+
+ plan(27);
+
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("Fake PAM initialization failed");
+ args = putil_args_new(pamh, 0);
+ if (args == NULL)
+ bail("cannot create PAM argument struct");
+ TEST(putil_crit, LOG_CRIT, "putil_crit");
+ TEST(putil_err, LOG_ERR, "putil_err");
+ putil_debug(args, "%s", "foo");
+ ok(pam_output() == NULL, "putil_debug without debug on");
+ args->debug = true;
+ TEST(putil_debug, LOG_DEBUG, "putil_debug");
+ args->debug = false;
+
+ TEST_PAM(putil_crit_pam, PAM_SYSTEM_ERR, LOG_CRIT, "putil_crit_pam S");
+ TEST_PAM(putil_crit_pam, PAM_BUF_ERR, LOG_CRIT, "putil_crit_pam B");
+ TEST_PAM(putil_crit_pam, PAM_SUCCESS, LOG_CRIT, "putil_crit_pam ok");
+ TEST_PAM(putil_err_pam, PAM_SYSTEM_ERR, LOG_ERR, "putil_err_pam");
+ putil_debug_pam(args, PAM_SYSTEM_ERR, "%s", "bar");
+ ok(pam_output() == NULL, "putil_debug_pam without debug on");
+ args->debug = true;
+ TEST_PAM(putil_debug_pam, PAM_SYSTEM_ERR, LOG_DEBUG, "putil_debug_pam");
+ TEST_PAM(putil_debug_pam, PAM_SUCCESS, LOG_DEBUG, "putil_debug_pam ok");
+ args->debug = false;
+
+#ifdef HAVE_KRB5
+ TEST_KRB5(putil_crit_krb5, LOG_CRIT, "putil_crit_krb5");
+ TEST_KRB5(putil_err_krb5, LOG_ERR, "putil_err_krb5");
+ code = krb5_parse_name(args->ctx, "foo@bar@EXAMPLE.COM", &princ);
+ putil_debug_krb5(args, code, "%s", "krb");
+ ok(pam_output() == NULL, "putil_debug_krb5 without debug on");
+ args->debug = true;
+ TEST_KRB5(putil_debug_krb5, LOG_DEBUG, "putil_debug_krb5");
+ args->debug = false;
+#else
+ skip_block(4, "not built with Kerberos support");
+#endif
+
+ putil_args_free(args);
+ pam_end(pamh, 0);
+
+ return 0;
+}
diff --git a/tests/pam-util/options-t.c b/tests/pam-util/options-t.c
new file mode 100644
index 000000000000..f8b76730fb2d
--- /dev/null
+++ b/tests/pam-util/options-t.c
@@ -0,0 +1,458 @@
+/*
+ * PAM option parsing test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/pam.h>
+#include <portable/system.h>
+
+#include <syslog.h>
+
+#include <pam-util/args.h>
+#include <pam-util/options.h>
+#include <pam-util/vector.h>
+#include <tests/fakepam/pam.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+/* The configuration struct we will use for testing. */
+struct pam_config {
+ struct vector *cells;
+ bool debug;
+#ifdef HAVE_KRB5
+ krb5_deltat expires;
+#else
+ long expires;
+#endif
+ bool ignore_root;
+ long minimum_uid;
+ char *program;
+};
+
+#define K(name) (#name), offsetof(struct pam_config, name)
+
+/* The rules specifying the configuration options. */
+static struct option options[] = {
+ /* clang-format off */
+ { K(cells), true, LIST (NULL) },
+ { K(debug), true, BOOL (false) },
+ { K(expires), true, TIME (10) },
+ { K(ignore_root), false, BOOL (true) },
+ { K(minimum_uid), true, NUMBER (0) },
+ { K(program), true, STRING (NULL) },
+ /* clang-format on */
+};
+static const size_t optlen = sizeof(options) / sizeof(options[0]);
+
+/*
+ * A macro used to parse the various ways of spelling booleans. This reuses
+ * the argv_bool variable, setting it to the first value provided and then
+ * calling putil_args_parse() on it. It then checks whether the provided
+ * config option is set to the expected value.
+ */
+#define TEST_BOOL(a, c, v) \
+ do { \
+ argv_bool[0] = (a); \
+ status = putil_args_parse(args, 1, argv_bool, options, optlen); \
+ ok(status, "Parse of %s", (a)); \
+ is_int((v), (c), "...and value is correct"); \
+ ok(pam_output() == NULL, "...and no output"); \
+ } while (0)
+
+/*
+ * A macro used to test error reporting from putil_args_parse(). This reuses
+ * the argv_err variable, setting it to the first value provided and then
+ * calling putil_args_parse() on it. It then recovers the error message and
+ * expects it to match the severity and error message given.
+ */
+#define TEST_ERROR(a, p, e) \
+ do { \
+ argv_err[0] = (a); \
+ status = putil_args_parse(args, 1, argv_err, options, optlen); \
+ ok(status, "Parse of %s", (a)); \
+ seen = pam_output(); \
+ if (seen == NULL) \
+ ok_block(2, false, "...no error output"); \
+ else { \
+ is_int((p), seen->lines[0].priority, "...priority for %s", (a)); \
+ is_string((e), seen->lines[0].line, "...error for %s", (a)); \
+ } \
+ pam_output_free(seen); \
+ } while (0)
+
+
+/*
+ * Allocate and initialize a new struct config.
+ */
+static struct pam_config *
+config_new(void)
+{
+ return bcalloc(1, sizeof(struct pam_config));
+}
+
+
+/*
+ * Free a struct config and all of its members.
+ */
+static void
+config_free(struct pam_config *config)
+{
+ if (config == NULL)
+ return;
+ vector_free(config->cells);
+ free(config->program);
+ free(config);
+}
+
+
+int
+main(void)
+{
+ pam_handle_t *pamh;
+ struct pam_args *args;
+ struct pam_conv conv = {NULL, NULL};
+ bool status;
+ struct vector *cells;
+ char *program;
+ struct output *seen;
+ const char *argv_bool[2] = {NULL, NULL};
+ const char *argv_err[2] = {NULL, NULL};
+ const char *argv_empty[] = {NULL};
+#ifdef HAVE_KRB5
+ const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu",
+ "debug",
+ "expires=1d",
+ "ignore_root",
+ "minimum_uid=1000",
+ "program=/bin/true"};
+ char *krb5conf;
+#else
+ const char *argv_all[] = {"cells=stanford.edu,ir.stanford.edu",
+ "debug",
+ "expires=86400",
+ "ignore_root",
+ "minimum_uid=1000",
+ "program=/bin/true"};
+#endif
+
+ if (pam_start("test", NULL, &conv, &pamh) != PAM_SUCCESS)
+ sysbail("cannot create pam_handle_t");
+ args = putil_args_new(pamh, 0);
+ if (args == NULL)
+ bail("cannot create PAM argument struct");
+
+ plan(161);
+
+ /* First, check just the defaults. */
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting the defaults");
+ ok(args->config->cells == NULL, "...cells default");
+ is_int(false, args->config->debug, "...debug default");
+ is_int(10, args->config->expires, "...expires default");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(0, args->config->minimum_uid, "...minimum_uid default");
+ ok(args->config->program == NULL, "...program default");
+
+ /* Now parse an empty set of PAM arguments. Nothing should change. */
+ status = putil_args_parse(args, 0, argv_empty, options, optlen);
+ ok(status, "Parse of empty argv");
+ ok(args->config->cells == NULL, "...cells still default");
+ is_int(false, args->config->debug, "...debug still default");
+ is_int(10, args->config->expires, "...expires default");
+ is_int(true, args->config->ignore_root, "...ignore_root still default");
+ is_int(0, args->config->minimum_uid, "...minimum_uid still default");
+ ok(args->config->program == NULL, "...program still default");
+
+ /* Now, check setting everything. */
+ status = putil_args_parse(args, 6, argv_all, options, optlen);
+ ok(status, "Parse of full argv");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("stanford.edu", args->config->cells->strings[0],
+ "...first is stanford.edu");
+ is_string("ir.stanford.edu", args->config->cells->strings[1],
+ "...second is ir.stanford.edu");
+ }
+ is_int(true, args->config->debug, "...debug is set");
+ is_int(86400, args->config->expires, "...expires is set");
+ is_int(true, args->config->ignore_root, "...ignore_root is set");
+ is_int(1000, args->config->minimum_uid, "...minimum_uid is set");
+ is_string("/bin/true", args->config->program, "...program is set");
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test deep copying of defaults. */
+ cells = vector_new();
+ if (cells == NULL)
+ sysbail("cannot allocate memory");
+ vector_add(cells, "foo.com");
+ vector_add(cells, "bar.com");
+ options[0].defaults.list = cells;
+ program = strdup("/bin/false");
+ if (program == NULL)
+ sysbail("cannot allocate memory");
+ options[5].defaults.string = program;
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting defaults with new defaults");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("foo.com", args->config->cells->strings[0],
+ "...first is foo.com");
+ is_string("bar.com", args->config->cells->strings[1],
+ "...second is bar.com");
+ }
+ is_string("/bin/false", args->config->program, "...program is /bin/false");
+ status = putil_args_parse(args, 6, argv_all, options, optlen);
+ ok(status, "Parse of full argv after defaults");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("stanford.edu", args->config->cells->strings[0],
+ "...first is stanford.edu");
+ is_string("ir.stanford.edu", args->config->cells->strings[1],
+ "...second is ir.stanford.edu");
+ }
+ is_int(true, args->config->debug, "...debug is set");
+ is_int(86400, args->config->expires, "...expires is set");
+ is_int(true, args->config->ignore_root, "...ignore_root is set");
+ is_int(1000, args->config->minimum_uid, "...minimum_uid is set");
+ is_string("/bin/true", args->config->program, "...program is set");
+ is_string("foo.com", cells->strings[0], "...first cell after parse");
+ is_string("bar.com", cells->strings[1], "...second cell after parse");
+ is_string("/bin/false", program, "...string after parse");
+ config_free(args->config);
+ args->config = NULL;
+ is_string("foo.com", cells->strings[0], "...first cell after free");
+ is_string("bar.com", cells->strings[1], "...second cell after free");
+ is_string("/bin/false", program, "...string after free");
+ options[0].defaults.list = NULL;
+ options[5].defaults.string = NULL;
+ vector_free(cells);
+ free(program);
+
+ /* Test specifying the default for a vector parameter as a string. */
+ options[0].type = TYPE_STRLIST;
+ options[0].defaults.string = "foo.com,bar.com";
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting defaults with string default for vector");
+ if (args->config->cells == NULL)
+ ok_block(4, false, "...cells is set");
+ else {
+ ok(args->config->cells != NULL, "...cells is set");
+ is_int(2, args->config->cells->count, "...with two cells");
+ is_string("foo.com", args->config->cells->strings[0],
+ "...first is foo.com");
+ is_string("bar.com", args->config->cells->strings[1],
+ "...second is bar.com");
+ }
+ config_free(args->config);
+ args->config = NULL;
+ options[0].type = TYPE_LIST;
+ options[0].defaults.string = NULL;
+
+ /* Should be no errors so far. */
+ ok(pam_output() == NULL, "No errors so far");
+
+ /* Test various ways of spelling booleans. */
+ args->config = config_new();
+ TEST_BOOL("debug", args->config->debug, true);
+ TEST_BOOL("debug=false", args->config->debug, false);
+ TEST_BOOL("debug=true", args->config->debug, true);
+ TEST_BOOL("debug=no", args->config->debug, false);
+ TEST_BOOL("debug=yes", args->config->debug, true);
+ TEST_BOOL("debug=off", args->config->debug, false);
+ TEST_BOOL("debug=on", args->config->debug, true);
+ TEST_BOOL("debug=0", args->config->debug, false);
+ TEST_BOOL("debug=1", args->config->debug, true);
+ TEST_BOOL("debug=False", args->config->debug, false);
+ TEST_BOOL("debug=trUe", args->config->debug, true);
+ TEST_BOOL("debug=No", args->config->debug, false);
+ TEST_BOOL("debug=Yes", args->config->debug, true);
+ TEST_BOOL("debug=OFF", args->config->debug, false);
+ TEST_BOOL("debug=ON", args->config->debug, true);
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test for various parsing errors. */
+ args->config = config_new();
+ TEST_ERROR("debug=", LOG_ERR, "invalid boolean in setting: debug=");
+ TEST_ERROR("debug=truth", LOG_ERR,
+ "invalid boolean in setting: debug=truth");
+ TEST_ERROR("minimum_uid", LOG_ERR, "value missing for option minimum_uid");
+ TEST_ERROR("minimum_uid=", LOG_ERR,
+ "value missing for option minimum_uid=");
+ TEST_ERROR("minimum_uid=foo", LOG_ERR,
+ "invalid number in setting: minimum_uid=foo");
+ TEST_ERROR("minimum_uid=1000foo", LOG_ERR,
+ "invalid number in setting: minimum_uid=1000foo");
+ TEST_ERROR("program", LOG_ERR, "value missing for option program");
+ TEST_ERROR("cells", LOG_ERR, "value missing for option cells");
+ config_free(args->config);
+ args->config = NULL;
+
+#ifdef HAVE_KRB5
+
+ /* Test for Kerberos krb5.conf option parsing. */
+ krb5conf = test_file_path("data/krb5-pam.conf");
+ if (krb5conf == NULL)
+ bail("cannot find data/krb5-pam.conf");
+ if (setenv("KRB5_CONFIG", krb5conf, 1) < 0)
+ sysbail("cannot set KRB5_CONFIG");
+ krb5_free_context(args->ctx);
+ status = krb5_init_context(&args->ctx);
+ if (status != 0)
+ bail("cannot parse test krb5.conf file");
+ args->config = config_new();
+ status = putil_args_defaults(args, options, optlen);
+ ok(status, "Setting the defaults");
+ status = putil_args_krb5(args, "testing", options, optlen);
+ ok(status, "Options from krb5.conf");
+ ok(args->config->cells == NULL, "...cells default");
+ is_int(true, args->config->debug, "...debug set from krb5.conf");
+ is_int(1800, args->config->expires, "...expires set from krb5.conf");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf");
+ ok(args->config->program == NULL, "...program default");
+ status = putil_args_krb5(args, "other-test", options, optlen);
+ ok(status, "Options from krb5.conf (other-test)");
+ is_int(-1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf other-test");
+
+ /* Test with a realm set, which should expose more settings. */
+ krb5_free_context(args->ctx);
+ status = krb5_init_context(&args->ctx);
+ if (status != 0)
+ bail("cannot parse test krb5.conf file");
+ args->realm = strdup("FOO.COM");
+ if (args->realm == NULL)
+ sysbail("cannot allocate memory");
+ status = putil_args_krb5(args, "testing", options, optlen);
+ ok(status, "Options from krb5.conf with FOO.COM");
+ is_int(2, args->config->cells->count, "...cells count from krb5.conf");
+ is_string("foo.com", args->config->cells->strings[0],
+ "...first cell from krb5.conf");
+ is_string("bar.com", args->config->cells->strings[1],
+ "...second cell from krb5.conf");
+ is_int(true, args->config->debug, "...debug set from krb5.conf");
+ is_int(1800, args->config->expires, "...expires set from krb5.conf");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf");
+ is_string("/bin/false", args->config->program,
+ "...program from krb5.conf");
+
+ /* Test with a different realm. */
+ free(args->realm);
+ args->realm = strdup("BAR.COM");
+ if (args->realm == NULL)
+ sysbail("cannot allocate memory");
+ status = putil_args_krb5(args, "testing", options, optlen);
+ ok(status, "Options from krb5.conf with BAR.COM");
+ is_int(2, args->config->cells->count, "...cells count from krb5.conf");
+ is_string("bar.com", args->config->cells->strings[0],
+ "...first cell from krb5.conf");
+ is_string("foo.com", args->config->cells->strings[1],
+ "...second cell from krb5.conf");
+ is_int(true, args->config->debug, "...debug set from krb5.conf");
+ is_int(1800, args->config->expires, "...expires set from krb5.conf");
+ is_int(true, args->config->ignore_root, "...ignore_root default");
+ is_int(1000, args->config->minimum_uid,
+ "...minimum_uid set from krb5.conf");
+ is_string("echo /bin/true", args->config->program,
+ "...program from krb5.conf");
+ config_free(args->config);
+ args->config = config_new();
+ status = putil_args_krb5(args, "other-test", options, optlen);
+ ok(status, "Options from krb5.conf (other-test with realm)");
+ ok(args->config->cells == NULL, "...cells is NULL");
+ is_string("echo /bin/true", args->config->program,
+ "...program from krb5.conf");
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test for time parsing errors. */
+ args->config = config_new();
+ TEST_ERROR("expires=ft87", LOG_ERR,
+ "bad time value in setting: expires=ft87");
+ config_free(args->config);
+
+ /* Test error reporting from the krb5.conf parser. */
+ args->config = config_new();
+ status = putil_args_krb5(args, "bad-number", options, optlen);
+ ok(status, "Options from krb5.conf (bad-number)");
+ seen = pam_output();
+ is_string("invalid number in krb5.conf setting for minimum_uid: 1000foo",
+ seen->lines[0].line, "...and correct error reported");
+ is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority");
+ pam_output_free(seen);
+ config_free(args->config);
+ args->config = NULL;
+
+ /* Test error reporting on times from the krb5.conf parser. */
+ args->config = config_new();
+ status = putil_args_krb5(args, "bad-time", options, optlen);
+ ok(status, "Options from krb5.conf (bad-time)");
+ seen = pam_output();
+ if (seen == NULL)
+ ok_block(2, false, "...no error output");
+ else {
+ is_string("invalid time in krb5.conf setting for expires: ft87",
+ seen->lines[0].line, "...and correct error reported");
+ is_int(LOG_ERR, seen->lines[0].priority, "...with correct priority");
+ }
+ pam_output_free(seen);
+ config_free(args->config);
+ args->config = NULL;
+
+ test_file_path_free(krb5conf);
+
+#else /* !HAVE_KRB5 */
+
+ skip_block(37, "Kerberos support not configured");
+
+#endif
+
+ putil_args_free(args);
+ pam_end(pamh, 0);
+ return 0;
+}
diff --git a/tests/pam-util/vector-t.c b/tests/pam-util/vector-t.c
new file mode 100644
index 000000000000..d7b87e36d8f4
--- /dev/null
+++ b/tests/pam-util/vector-t.c
@@ -0,0 +1,149 @@
+/*
+ * PAM utility vector library test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014, 2016, 2018-2019 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2010-2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <sys/wait.h>
+
+#include <pam-util/vector.h>
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+
+int
+main(void)
+{
+ struct vector *vector, *ovector, *copy;
+ char *command, *string;
+ const char *env[2];
+ pid_t child;
+ size_t i;
+ const char cstring[] = "This is a\ttest. ";
+
+ plan(60);
+
+ vector = vector_new();
+ ok(vector != NULL, "vector_new returns non-NULL");
+ if (vector == NULL)
+ bail("vector_new returned NULL");
+ ok(vector_add(vector, cstring), "vector_add succeeds");
+ is_int(1, vector->count, "vector_add increases count");
+ ok(vector->strings[0] != cstring, "...and allocated new memory");
+ ok(vector_resize(vector, 4), "vector_resize succeeds");
+ is_int(4, vector->allocated, "vector_resize works");
+ ok(vector_add(vector, cstring), "vector_add #2");
+ ok(vector_add(vector, cstring), "vector_add #3");
+ ok(vector_add(vector, cstring), "vector_add #4");
+ is_int(4, vector->allocated, "...and no reallocation when adding strings");
+ is_int(4, vector->count, "...and the count matches");
+ is_string(cstring, vector->strings[0], "added the right string");
+ is_string(cstring, vector->strings[1], "added the right string");
+ is_string(cstring, vector->strings[2], "added the right string");
+ is_string(cstring, vector->strings[3], "added the right string");
+ ok(vector->strings[1] != vector->strings[2], "each pointer is different");
+ ok(vector->strings[2] != vector->strings[3], "each pointer is different");
+ ok(vector->strings[3] != vector->strings[0], "each pointer is different");
+ ok(vector->strings[0] != cstring, "each pointer is different");
+ copy = vector_copy(vector);
+ ok(copy != NULL, "vector_copy returns non-NULL");
+ if (copy == NULL)
+ bail("vector_copy returned NULL");
+ is_int(4, copy->count, "...and has right count");
+ is_int(4, copy->allocated, "...and has right allocated count");
+ for (i = 0; i < 4; i++) {
+ is_string(cstring, copy->strings[i], "...and string %lu is right",
+ (unsigned long) i);
+ ok(copy->strings[i] != vector->strings[i],
+ "...and pointer %lu is different", (unsigned long) i);
+ }
+ vector_free(copy);
+ vector_clear(vector);
+ is_int(0, vector->count, "vector_clear works");
+ is_int(4, vector->allocated, "...but doesn't free the allocation");
+ string = strdup(cstring);
+ if (string == NULL)
+ sysbail("cannot allocate memory");
+ ok(vector_add(vector, cstring), "vector_add succeeds");
+ ok(vector_add(vector, string), "vector_add succeeds");
+ is_int(2, vector->count, "added two strings to the vector");
+ ok(vector->strings[1] != string, "...and the pointers are different");
+ ok(vector_resize(vector, 1), "vector_resize succeeds");
+ is_int(1, vector->count, "vector_resize shrinks the vector");
+ ok(vector->strings[0] != cstring, "...and the pointer is different");
+ vector_free(vector);
+ free(string);
+
+ vector = vector_split_multi("foo, bar, baz", ", ", NULL);
+ ok(vector != NULL, "vector_split_multi returns non-NULL");
+ if (vector == NULL)
+ bail("vector_split_multi returned NULL");
+ is_int(3, vector->count, "vector_split_multi returns right count");
+ is_string("foo", vector->strings[0], "...first string");
+ is_string("bar", vector->strings[1], "...second string");
+ is_string("baz", vector->strings[2], "...third string");
+ ovector = vector;
+ vector = vector_split_multi("", ", ", vector);
+ ok(vector != NULL, "reuse of vector doesn't return NULL");
+ ok(vector == ovector, "...and reuses the same vector pointer");
+ is_int(0, vector->count, "vector_split_multi reuse with empty string");
+ is_int(3, vector->allocated, "...and doesn't free allocation");
+ vector = vector_split_multi(",,, foo, ", ", ", vector);
+ ok(vector != NULL, "reuse of vector doesn't return NULL");
+ is_int(1, vector->count, "vector_split_multi with extra separators");
+ is_string("foo", vector->strings[0], "...first string");
+ vector = vector_split_multi(", , ", ", ", vector);
+ is_int(0, vector->count, "vector_split_multi with only separators");
+ vector_free(vector);
+
+ vector = vector_new();
+ ok(vector_add(vector, "/bin/sh"), "vector_add succeeds");
+ ok(vector_add(vector, "-c"), "vector_add succeeds");
+ basprintf(&command, "echo ok %lu - vector_exec", testnum++);
+ ok(vector_add(vector, command), "vector_add succeeds");
+ child = fork();
+ if (child < 0)
+ sysbail("unable to fork");
+ else if (child == 0)
+ if (vector_exec("/bin/sh", vector) < 0)
+ sysdiag("unable to exec /bin/sh");
+ waitpid(child, NULL, 0);
+ vector_free(vector);
+ free(command);
+
+ vector = vector_new();
+ ok(vector_add(vector, "/bin/sh"), "vector_add succeeds");
+ ok(vector_add(vector, "-c"), "vector_add succeeds");
+ ok(vector_add(vector, "echo ok $NUMBER - vector_exec_env"),
+ "vector_add succeeds");
+ basprintf(&string, "NUMBER=%lu", testnum++);
+ env[0] = string;
+ env[1] = NULL;
+ child = fork();
+ if (child < 0)
+ sysbail("unable to fork");
+ else if (child == 0)
+ if (vector_exec_env("/bin/sh", vector, env) < 0)
+ sysdiag("unable to exec /bin/sh");
+ waitpid(child, NULL, 0);
+ vector_free(vector);
+ free(string);
+
+ return 0;
+}
diff --git a/tests/portable/asprintf-t.c b/tests/portable/asprintf-t.c
new file mode 100644
index 000000000000..3b10a6622c31
--- /dev/null
+++ b/tests/portable/asprintf-t.c
@@ -0,0 +1,69 @@
+/*
+ * asprintf and vasprintf test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2014, 2018 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2009, 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/macros.h>
+#include <portable/system.h>
+
+#include <tests/tap/basic.h>
+
+int test_asprintf(char **, const char *, ...)
+ __attribute__((__format__(printf, 2, 3)));
+int test_vasprintf(char **, const char *, va_list)
+ __attribute__((__format__(printf, 2, 0)));
+
+static int __attribute__((__format__(printf, 2, 3)))
+vatest(char **result, const char *format, ...)
+{
+ va_list args;
+ int status;
+
+ va_start(args, format);
+ status = test_vasprintf(result, format, args);
+ va_end(args);
+ return status;
+}
+
+int
+main(void)
+{
+ char *result = NULL;
+
+ plan(12);
+
+ is_int(7, test_asprintf(&result, "%s", "testing"), "asprintf length");
+ is_string("testing", result, "asprintf result");
+ free(result);
+ ok(3, "free asprintf");
+ is_int(0, test_asprintf(&result, "%s", ""), "asprintf empty length");
+ is_string("", result, "asprintf empty string");
+ free(result);
+ ok(6, "free asprintf of empty string");
+
+ is_int(6, vatest(&result, "%d %s", 2, "test"), "vasprintf length");
+ is_string("2 test", result, "vasprintf result");
+ free(result);
+ ok(9, "free vasprintf");
+ is_int(0, vatest(&result, "%s", ""), "vasprintf empty length");
+ is_string("", result, "vasprintf empty string");
+ free(result);
+ ok(12, "free vasprintf of empty string");
+
+ return 0;
+}
diff --git a/tests/portable/asprintf.c b/tests/portable/asprintf.c
new file mode 100644
index 000000000000..221c9932c5cd
--- /dev/null
+++ b/tests/portable/asprintf.c
@@ -0,0 +1,2 @@
+#define TESTING 1
+#include <portable/asprintf.c>
diff --git a/tests/portable/mkstemp-t.c b/tests/portable/mkstemp-t.c
new file mode 100644
index 000000000000..dc268210f063
--- /dev/null
+++ b/tests/portable/mkstemp-t.c
@@ -0,0 +1,81 @@
+/*
+ * mkstemp test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009, 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <sys/stat.h>
+
+#include <tests/tap/basic.h>
+
+int test_mkstemp(char *template);
+
+int
+main(void)
+{
+ int fd;
+ char template[] = "tsXXXXXXX";
+ char tooshort[] = "XXXXX";
+ char bad1[] = "/foo/barXXXXX";
+ char bad2[] = "/foo/barXXXXXX.out";
+ char buffer[256];
+ struct stat st1, st2;
+ ssize_t length;
+
+ plan(20);
+
+ /* First, test a few error messages. */
+ errno = 0;
+ is_int(-1, test_mkstemp(tooshort), "too short of template");
+ is_int(EINVAL, errno, "...with correct errno");
+ is_string("XXXXX", tooshort, "...and template didn't change");
+ errno = 0;
+ is_int(-1, test_mkstemp(bad1), "bad template");
+ is_int(EINVAL, errno, "...with correct errno");
+ is_string("/foo/barXXXXX", bad1, "...and template didn't change");
+ errno = 0;
+ is_int(-1, test_mkstemp(bad2), "template doesn't end in XXXXXX");
+ is_int(EINVAL, errno, "...with correct errno");
+ is_string("/foo/barXXXXXX.out", bad2, "...and template didn't change");
+ errno = 0;
+
+ /* Now try creating a real file. */
+ fd = test_mkstemp(template);
+ ok(fd >= 0, "mkstemp works with valid template");
+ ok(strcmp(template, "tsXXXXXXX") != 0, "...and template changed");
+ ok(strncmp(template, "tsX", 3) == 0, "...and didn't touch first X");
+ ok(access(template, F_OK) == 0, "...and the file exists");
+
+ /* Make sure that it's the same file as template refers to now. */
+ ok(stat(template, &st1) == 0, "...and stat of template works");
+ ok(fstat(fd, &st2) == 0, "...and stat of open file descriptor works");
+ ok(st1.st_ino == st2.st_ino, "...and they're the same file");
+ unlink(template);
+
+ /* Make sure the open mode is correct. */
+ length = strlen(template);
+ is_int(length, write(fd, template, length), "write to open file works");
+ ok(lseek(fd, 0, SEEK_SET) == 0, "...and rewind works");
+ is_int(length, read(fd, buffer, length), "...and the data is there");
+ buffer[length] = '\0';
+ is_string(template, buffer, "...and matches what we wrote");
+ close(fd);
+
+ return 0;
+}
diff --git a/tests/portable/mkstemp.c b/tests/portable/mkstemp.c
new file mode 100644
index 000000000000..4632d3de86ed
--- /dev/null
+++ b/tests/portable/mkstemp.c
@@ -0,0 +1,2 @@
+#define TESTING 1
+#include <portable/mkstemp.c>
diff --git a/tests/portable/strndup-t.c b/tests/portable/strndup-t.c
new file mode 100644
index 000000000000..9bf28a31beec
--- /dev/null
+++ b/tests/portable/strndup-t.c
@@ -0,0 +1,60 @@
+/*
+ * strndup test suite.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2018 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011-2012
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Copying and distribution of this file, with or without modification, are
+ * permitted in any medium without royalty provided the copyright notice and
+ * this notice are preserved. This file is offered as-is, without any
+ * warranty.
+ *
+ * SPDX-License-Identifier: FSFAP
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+
+#include <tests/tap/basic.h>
+
+char *test_strndup(const char *, size_t);
+
+
+int
+main(void)
+{
+ char buffer[3];
+ char *result;
+
+ plan(7);
+
+ result = test_strndup("foo", 8);
+ is_string("foo", result, "strndup longer than string");
+ free(result);
+ result = test_strndup("foo", 2);
+ is_string("fo", result, "strndup shorter than string");
+ free(result);
+ result = test_strndup("foo", 3);
+ is_string("foo", result, "strndup same size as string");
+ free(result);
+ result = test_strndup("foo", 0);
+ is_string("", result, "strndup of size 0");
+ free(result);
+ memcpy(buffer, "foo", 3);
+ result = test_strndup(buffer, 3);
+ is_string("foo", result, "strndup of non-nul-terminated string");
+ free(result);
+ errno = 0;
+ result = test_strndup(NULL, 0);
+ is_string(NULL, result, "strndup of NULL");
+ is_int(errno, EINVAL, "...and returns EINVAL");
+
+ return 0;
+}
diff --git a/tests/portable/strndup.c b/tests/portable/strndup.c
new file mode 100644
index 000000000000..99c3bc13a744
--- /dev/null
+++ b/tests/portable/strndup.c
@@ -0,0 +1,2 @@
+#define TESTING 1
+#include <portable/strndup.c>
diff --git a/tests/runtests.c b/tests/runtests.c
new file mode 100644
index 000000000000..54ec1c93d08b
--- /dev/null
+++ b/tests/runtests.c
@@ -0,0 +1,1782 @@
+/*
+ * Run a set of tests, reporting results.
+ *
+ * Test suite driver that runs a set of tests implementing a subset of the
+ * Test Anything Protocol (TAP) and reports the results.
+ *
+ * Any bug reports, bug fixes, and improvements are very much welcome and
+ * should be sent to the e-mail address below. This program is part of C TAP
+ * Harness <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Copyright 2000-2001, 2004, 2006-2019 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/*
+ * Usage:
+ *
+ * runtests [-hv] [-b <build-dir>] [-s <source-dir>] -l <test-list>
+ * runtests [-hv] [-b <build-dir>] [-s <source-dir>] <test> [<test> ...]
+ * runtests -o [-h] [-b <build-dir>] [-s <source-dir>] <test>
+ *
+ * In the first case, expects a list of executables located in the given file,
+ * one line per executable, possibly followed by a space-separated list of
+ * options. For each one, runs it as part of a test suite, reporting results.
+ * In the second case, use the same infrastructure, but run only the tests
+ * listed on the command line.
+ *
+ * Test output should start with a line containing the number of tests
+ * (numbered from 1 to this number), optionally preceded by "1..", although
+ * that line may be given anywhere in the output. Each additional line should
+ * be in the following format:
+ *
+ * ok <number>
+ * not ok <number>
+ * ok <number> # skip
+ * not ok <number> # todo
+ *
+ * where <number> is the number of the test. An optional comment is permitted
+ * after the number if preceded by whitespace. ok indicates success, not ok
+ * indicates failure. "# skip" and "# todo" are a special cases of a comment,
+ * and must start with exactly that formatting. They indicate the test was
+ * skipped for some reason (maybe because it doesn't apply to this platform)
+ * or is testing something known to currently fail. The text following either
+ * "# skip" or "# todo" and whitespace is the reason.
+ *
+ * As a special case, the first line of the output may be in the form:
+ *
+ * 1..0 # skip some reason
+ *
+ * which indicates that this entire test case should be skipped and gives a
+ * reason.
+ *
+ * Any other lines are ignored, although for compliance with the TAP protocol
+ * all lines other than the ones in the above format should be sent to
+ * standard error rather than standard output and start with #.
+ *
+ * This is a subset of TAP as documented in Test::Harness::TAP or
+ * TAP::Parser::Grammar, which comes with Perl.
+ *
+ * If the -o option is given, instead run a single test and display all of its
+ * output. This is intended for use with failing tests so that the person
+ * running the test suite can get more details about what failed.
+ *
+ * If built with the C preprocessor symbols C_TAP_SOURCE and C_TAP_BUILD
+ * defined, C TAP Harness will export those values in the environment so that
+ * tests can find the source and build directory and will look for tests under
+ * both directories. These paths can also be set with the -b and -s
+ * command-line options, which will override anything set at build time.
+ *
+ * If the -v option is given, or the C_TAP_VERBOSE environment variable is set,
+ * display the full output of each test as it runs rather than showing a
+ * summary of the results of each test.
+ */
+
+/* Required for fdopen(), getopt(), and putenv(). */
+#if defined(__STRICT_ANSI__) || defined(PEDANTIC)
+# ifndef _XOPEN_SOURCE
+# define _XOPEN_SOURCE 500
+# endif
+#endif
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+/* sys/time.h must be included before sys/resource.h on some platforms. */
+#include <sys/resource.h>
+
+/* AIX 6.1 (and possibly later) doesn't have WCOREDUMP. */
+#ifndef WCOREDUMP
+# define WCOREDUMP(status) ((unsigned) (status) &0x80)
+#endif
+
+/*
+ * POSIX requires that these be defined in <unistd.h>, but they're not always
+ * available. If one of them has been defined, all the rest almost certainly
+ * have.
+ */
+#ifndef STDIN_FILENO
+# define STDIN_FILENO 0
+# define STDOUT_FILENO 1
+# define STDERR_FILENO 2
+#endif
+
+/*
+ * Used for iterating through arrays. Returns the number of elements in the
+ * array (useful for a < upper bound in a for loop).
+ */
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+
+/*
+ * The source and build versions of the tests directory. This is used to set
+ * the C_TAP_SOURCE and C_TAP_BUILD environment variables (and the SOURCE and
+ * BUILD environment variables set for backward compatibility) and find test
+ * programs, if set. Normally, this should be set as part of the build
+ * process to the test subdirectories of $(abs_top_srcdir) and
+ * $(abs_top_builddir) respectively.
+ */
+#ifndef C_TAP_SOURCE
+# define C_TAP_SOURCE NULL
+#endif
+#ifndef C_TAP_BUILD
+# define C_TAP_BUILD NULL
+#endif
+
+/* Test status codes. */
+enum test_status
+{
+ TEST_FAIL,
+ TEST_PASS,
+ TEST_SKIP,
+ TEST_INVALID
+};
+
+/* Really, just a boolean, but this is more self-documenting. */
+enum test_verbose
+{
+ CONCISE = 0,
+ VERBOSE = 1
+};
+
+/* Indicates the state of our plan. */
+enum plan_status
+{
+ PLAN_INIT, /* Nothing seen yet. */
+ PLAN_FIRST, /* Plan seen before any tests. */
+ PLAN_PENDING, /* Test seen and no plan yet. */
+ PLAN_FINAL /* Plan seen after some tests. */
+};
+
+/* Error exit statuses for test processes. */
+#define CHILDERR_DUP 100 /* Couldn't redirect stderr or stdout. */
+#define CHILDERR_EXEC 101 /* Couldn't exec child process. */
+#define CHILDERR_STDIN 102 /* Couldn't open stdin file. */
+#define CHILDERR_STDERR 103 /* Couldn't open stderr file. */
+
+/* Structure to hold data for a set of tests. */
+struct testset {
+ char *file; /* The file name of the test. */
+ char **command; /* The argv vector to run the command. */
+ enum plan_status plan; /* The status of our plan. */
+ unsigned long count; /* Expected count of tests. */
+ unsigned long current; /* The last seen test number. */
+ unsigned int length; /* The length of the last status message. */
+ unsigned long passed; /* Count of passing tests. */
+ unsigned long failed; /* Count of failing lists. */
+ unsigned long skipped; /* Count of skipped tests (passed). */
+ unsigned long allocated; /* The size of the results table. */
+ enum test_status *results; /* Table of results by test number. */
+ unsigned int aborted; /* Whether the set was aborted. */
+ unsigned int reported; /* Whether the results were reported. */
+ int status; /* The exit status of the test. */
+ unsigned int all_skipped; /* Whether all tests were skipped. */
+ char *reason; /* Why all tests were skipped. */
+};
+
+/* Structure to hold a linked list of test sets. */
+struct testlist {
+ struct testset *ts;
+ struct testlist *next;
+};
+
+/*
+ * Usage message. Should be used as a printf format with four arguments: the
+ * path to runtests, given three times, and the usage_description. This is
+ * split into variables to satisfy the pedantic ISO C90 limit on strings.
+ */
+static const char usage_message[] = "\
+Usage: %s [-hv] [-b <build-dir>] [-s <source-dir>] <test> ...\n\
+ %s [-hv] [-b <build-dir>] [-s <source-dir>] -l <test-list>\n\
+ %s -o [-h] [-b <build-dir>] [-s <source-dir>] <test>\n\
+\n\
+Options:\n\
+ -b <build-dir> Set the build directory to <build-dir>\n\
+%s";
+static const char usage_extra[] = "\
+ -l <list> Take the list of tests to run from <test-list>\n\
+ -o Run a single test rather than a list of tests\n\
+ -s <source-dir> Set the source directory to <source-dir>\n\
+ -v Show the full output of each test\n\
+\n\
+runtests normally runs each test listed on the command line. With the -l\n\
+option, it instead runs every test listed in a file. With the -o option,\n\
+it instead runs a single test and shows its complete output.\n";
+
+/*
+ * Header used for test output. %s is replaced by the file name of the list
+ * of tests.
+ */
+static const char banner[] = "\n\
+Running all tests listed in %s. If any tests fail, run the failing\n\
+test program with runtests -o to see more details.\n\n";
+
+/* Header for reports of failed tests. */
+static const char header[] = "\n\
+Failed Set Fail/Total (%) Skip Stat Failing Tests\n\
+-------------------------- -------------- ---- ---- ------------------------";
+
+/* Include the file name and line number in malloc failures. */
+#define xcalloc(n, type) \
+ ((type *) x_calloc((n), sizeof(type), __FILE__, __LINE__))
+#define xmalloc(size) ((char *) x_malloc((size), __FILE__, __LINE__))
+#define xstrdup(p) x_strdup((p), __FILE__, __LINE__)
+#define xstrndup(p, size) x_strndup((p), (size), __FILE__, __LINE__)
+#define xreallocarray(p, n, type) \
+ ((type *) x_reallocarray((p), (n), sizeof(type), __FILE__, __LINE__))
+
+/*
+ * __attribute__ is available in gcc 2.5 and later, but only with gcc 2.7
+ * could you use the __format__ form of the attributes, which is what we use
+ * (to avoid confusion with other macros).
+ */
+#ifndef __attribute__
+# if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 7)
+# define __attribute__(spec) /* empty */
+# endif
+#endif
+
+/*
+ * We use __alloc_size__, but it was only available in fairly recent versions
+ * of GCC. Suppress warnings about the unknown attribute if GCC is too old.
+ * We know that we're GCC at this point, so we can use the GCC variadic macro
+ * extension, which will still work with versions of GCC too old to have C99
+ * variadic macro support.
+ */
+#if !defined(__attribute__) && !defined(__alloc_size__)
+# if defined(__GNUC__) && !defined(__clang__)
+# if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 3)
+# define __alloc_size__(spec, args...) /* empty */
+# endif
+# endif
+#endif
+
+/*
+ * LLVM and Clang pretend to be GCC but don't support all of the __attribute__
+ * settings that GCC does. For them, suppress warnings about unknown
+ * attributes on declarations. This unfortunately will affect the entire
+ * compilation context, but there's no push and pop available.
+ */
+#if !defined(__attribute__) && (defined(__llvm__) || defined(__clang__))
+# pragma GCC diagnostic ignored "-Wattributes"
+#endif
+
+/* Declare internal functions that benefit from compiler attributes. */
+static void die(const char *, ...)
+ __attribute__((__nonnull__, __noreturn__, __format__(printf, 1, 2)));
+static void sysdie(const char *, ...)
+ __attribute__((__nonnull__, __noreturn__, __format__(printf, 1, 2)));
+static void *x_calloc(size_t, size_t, const char *, int)
+ __attribute__((__alloc_size__(1, 2), __malloc__, __nonnull__));
+static void *x_malloc(size_t, const char *, int)
+ __attribute__((__alloc_size__(1), __malloc__, __nonnull__));
+static void *x_reallocarray(void *, size_t, size_t, const char *, int)
+ __attribute__((__alloc_size__(2, 3), __malloc__, __nonnull__(4)));
+static char *x_strdup(const char *, const char *, int)
+ __attribute__((__malloc__, __nonnull__));
+static char *x_strndup(const char *, size_t, const char *, int)
+ __attribute__((__malloc__, __nonnull__));
+
+
+/*
+ * Report a fatal error and exit.
+ */
+static void
+die(const char *format, ...)
+{
+ va_list args;
+
+ fflush(stdout);
+ fprintf(stderr, "runtests: ");
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+ fprintf(stderr, "\n");
+ exit(1);
+}
+
+
+/*
+ * Report a fatal error, including the results of strerror, and exit.
+ */
+static void
+sysdie(const char *format, ...)
+{
+ int oerrno;
+ va_list args;
+
+ oerrno = errno;
+ fflush(stdout);
+ fprintf(stderr, "runtests: ");
+ va_start(args, format);
+ vfprintf(stderr, format, args);
+ va_end(args);
+ fprintf(stderr, ": %s\n", strerror(oerrno));
+ exit(1);
+}
+
+
+/*
+ * Allocate zeroed memory, reporting a fatal error and exiting on failure.
+ */
+static void *
+x_calloc(size_t n, size_t size, const char *file, int line)
+{
+ void *p;
+
+ n = (n > 0) ? n : 1;
+ size = (size > 0) ? size : 1;
+ p = calloc(n, size);
+ if (p == NULL)
+ sysdie("failed to calloc %lu bytes at %s line %d",
+ (unsigned long) size, file, line);
+ return p;
+}
+
+
+/*
+ * Allocate memory, reporting a fatal error and exiting on failure.
+ */
+static void *
+x_malloc(size_t size, const char *file, int line)
+{
+ void *p;
+
+ p = malloc(size);
+ if (p == NULL)
+ sysdie("failed to malloc %lu bytes at %s line %d",
+ (unsigned long) size, file, line);
+ return p;
+}
+
+
+/*
+ * Reallocate memory, reporting a fatal error and exiting on failure.
+ *
+ * We should technically use SIZE_MAX here for the overflow check, but
+ * SIZE_MAX is C99 and we're only assuming C89 + SUSv3, which does not
+ * guarantee that it exists. They do guarantee that UINT_MAX exists, and we
+ * can assume that UINT_MAX <= SIZE_MAX. And we should not be allocating
+ * anything anywhere near that large.
+ *
+ * (In theory, C89 and C99 permit size_t to be smaller than unsigned int, but
+ * I disbelieve in the existence of such systems and they will have to cope
+ * without overflow checks.)
+ */
+static void *
+x_reallocarray(void *p, size_t n, size_t size, const char *file, int line)
+{
+ n = (n > 0) ? n : 1;
+ size = (size > 0) ? size : 1;
+
+ if (n > 0 && UINT_MAX / n <= size)
+ sysdie("realloc too large at %s line %d", file, line);
+ p = realloc(p, n * size);
+ if (p == NULL)
+ sysdie("failed to realloc %lu bytes at %s line %d",
+ (unsigned long) (n * size), file, line);
+ return p;
+}
+
+
+/*
+ * Copy a string, reporting a fatal error and exiting on failure.
+ */
+static char *
+x_strdup(const char *s, const char *file, int line)
+{
+ char *p;
+ size_t len;
+
+ len = strlen(s) + 1;
+ p = (char *) malloc(len);
+ if (p == NULL)
+ sysdie("failed to strdup %lu bytes at %s line %d", (unsigned long) len,
+ file, line);
+ memcpy(p, s, len);
+ return p;
+}
+
+
+/*
+ * Copy the first n characters of a string, reporting a fatal error and
+ * existing on failure.
+ *
+ * Avoid using the system strndup function since it may not exist (on Mac OS
+ * X, for example), and there's no need to introduce another portability
+ * requirement.
+ */
+char *
+x_strndup(const char *s, size_t size, const char *file, int line)
+{
+ const char *p;
+ size_t len;
+ char *copy;
+
+ /* Don't assume that the source string is nul-terminated. */
+ for (p = s; (size_t)(p - s) < size && *p != '\0'; p++)
+ ;
+ len = (size_t)(p - s);
+ copy = (char *) malloc(len + 1);
+ if (copy == NULL)
+ sysdie("failed to strndup %lu bytes at %s line %d",
+ (unsigned long) len, file, line);
+ memcpy(copy, s, len);
+ copy[len] = '\0';
+ return copy;
+}
+
+
+/*
+ * Form a new string by concatenating multiple strings. The arguments must be
+ * terminated by (const char *) 0.
+ *
+ * This function only exists because we can't assume asprintf. We can't
+ * simulate asprintf with snprintf because we're only assuming SUSv3, which
+ * does not require that snprintf with a NULL buffer return the required
+ * length. When those constraints are relaxed, this should be ripped out and
+ * replaced with asprintf or a more trivial replacement with snprintf.
+ */
+static char *
+concat(const char *first, ...)
+{
+ va_list args;
+ char *result;
+ const char *string;
+ size_t offset;
+ size_t length = 0;
+
+ /*
+ * Find the total memory required. Ensure we don't overflow length. We
+ * aren't guaranteed to have SIZE_MAX, so use UINT_MAX as an acceptable
+ * substitute (see the x_nrealloc comments).
+ */
+ va_start(args, first);
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ if (length >= UINT_MAX - strlen(string)) {
+ errno = EINVAL;
+ sysdie("strings too long in concat");
+ }
+ length += strlen(string);
+ }
+ va_end(args);
+ length++;
+
+ /* Create the string. */
+ result = xmalloc(length);
+ va_start(args, first);
+ offset = 0;
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ memcpy(result + offset, string, strlen(string));
+ offset += strlen(string);
+ }
+ va_end(args);
+ result[offset] = '\0';
+ return result;
+}
+
+
+/*
+ * Given a struct timeval, return the number of seconds it represents as a
+ * double. Use difftime() to convert a time_t to a double.
+ */
+static double
+tv_seconds(const struct timeval *tv)
+{
+ return difftime(tv->tv_sec, 0) + (double) tv->tv_usec * 1e-6;
+}
+
+
+/*
+ * Given two struct timevals, return the difference in seconds.
+ */
+static double
+tv_diff(const struct timeval *tv1, const struct timeval *tv0)
+{
+ return tv_seconds(tv1) - tv_seconds(tv0);
+}
+
+
+/*
+ * Given two struct timevals, return the sum in seconds as a double.
+ */
+static double
+tv_sum(const struct timeval *tv1, const struct timeval *tv2)
+{
+ return tv_seconds(tv1) + tv_seconds(tv2);
+}
+
+
+/*
+ * Given a pointer to a string, skip any leading whitespace and return a
+ * pointer to the first non-whitespace character.
+ */
+static const char *
+skip_whitespace(const char *p)
+{
+ while (isspace((unsigned char) (*p)))
+ p++;
+ return p;
+}
+
+
+/*
+ * Given a pointer to a string, skip any non-whitespace characters and return
+ * a pointer to the first whitespace character, or to the end of the string.
+ */
+static const char *
+skip_non_whitespace(const char *p)
+{
+ while (*p != '\0' && !isspace((unsigned char) (*p)))
+ p++;
+ return p;
+}
+
+
+/*
+ * Start a program, connecting its stdout to a pipe on our end and its stderr
+ * to /dev/null, and storing the file descriptor to read from in the two
+ * argument. Returns the PID of the new process. Errors are fatal.
+ */
+static pid_t
+test_start(char *const *command, int *fd)
+{
+ int fds[2], infd, errfd;
+ pid_t child;
+
+ /* Create a pipe used to capture the output from the test program. */
+ if (pipe(fds) == -1) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("can't create pipe");
+ }
+
+ /* Fork a child process, massage the file descriptors, and exec. */
+ child = fork();
+ switch (child) {
+ case -1:
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("can't fork");
+
+ /* In the child. Set up our standard output. */
+ case 0:
+ close(fds[0]);
+ close(STDOUT_FILENO);
+ if (dup2(fds[1], STDOUT_FILENO) < 0)
+ _exit(CHILDERR_DUP);
+ close(fds[1]);
+
+ /* Point standard input at /dev/null. */
+ close(STDIN_FILENO);
+ infd = open("/dev/null", O_RDONLY);
+ if (infd < 0)
+ _exit(CHILDERR_STDIN);
+ if (infd != STDIN_FILENO) {
+ if (dup2(infd, STDIN_FILENO) < 0)
+ _exit(CHILDERR_DUP);
+ close(infd);
+ }
+
+ /* Point standard error at /dev/null. */
+ close(STDERR_FILENO);
+ errfd = open("/dev/null", O_WRONLY);
+ if (errfd < 0)
+ _exit(CHILDERR_STDERR);
+ if (errfd != STDERR_FILENO) {
+ if (dup2(errfd, STDERR_FILENO) < 0)
+ _exit(CHILDERR_DUP);
+ close(errfd);
+ }
+
+ /* Now, exec our process. */
+ if (execv(command[0], command) == -1)
+ _exit(CHILDERR_EXEC);
+ break;
+
+ /* In parent. Close the extra file descriptor. */
+ default:
+ close(fds[1]);
+ break;
+ }
+ *fd = fds[0];
+ return child;
+}
+
+
+/*
+ * Back up over the output saying what test we were executing.
+ */
+static void
+test_backspace(struct testset *ts)
+{
+ unsigned int i;
+
+ if (!isatty(STDOUT_FILENO))
+ return;
+ for (i = 0; i < ts->length; i++)
+ putchar('\b');
+ for (i = 0; i < ts->length; i++)
+ putchar(' ');
+ for (i = 0; i < ts->length; i++)
+ putchar('\b');
+ ts->length = 0;
+}
+
+
+/*
+ * Allocate or resize the array of test results to be large enough to contain
+ * the test number in.
+ */
+static void
+resize_results(struct testset *ts, unsigned long n)
+{
+ unsigned long i;
+ size_t s;
+
+ /* If there's already enough space, return quickly. */
+ if (n <= ts->allocated)
+ return;
+
+ /*
+ * If no space has been allocated, do the initial allocation. Otherwise,
+ * resize. Start with 32 test cases and then add 1024 with each resize to
+ * try to reduce the number of reallocations.
+ */
+ if (ts->allocated == 0) {
+ s = (n > 32) ? n : 32;
+ ts->results = xcalloc(s, enum test_status);
+ } else {
+ s = (n > ts->allocated + 1024) ? n : ts->allocated + 1024;
+ ts->results = xreallocarray(ts->results, s, enum test_status);
+ }
+
+ /* Set the results for the newly-allocated test array. */
+ for (i = ts->allocated; i < s; i++)
+ ts->results[i] = TEST_INVALID;
+ ts->allocated = s;
+}
+
+
+/*
+ * Report an invalid test number and set the appropriate flags. Pulled into a
+ * separate function since we do this in several places.
+ */
+static void
+invalid_test_number(struct testset *ts, long n, enum test_verbose verbose)
+{
+ if (!verbose)
+ test_backspace(ts);
+ printf("ABORTED (invalid test number %ld)\n", n);
+ ts->aborted = 1;
+ ts->reported = 1;
+}
+
+
+/*
+ * Read the plan line of test output, which should contain the range of test
+ * numbers. We may initialize the testset structure here if we haven't yet
+ * seen a test. Return true if initialization succeeded and the test should
+ * continue, false otherwise.
+ */
+static int
+test_plan(const char *line, struct testset *ts, enum test_verbose verbose)
+{
+ long n;
+
+ /*
+ * Accept a plan without the leading 1.. for compatibility with older
+ * versions of runtests. This will only be allowed if we've not yet seen
+ * a test result.
+ */
+ line = skip_whitespace(line);
+ if (strncmp(line, "1..", 3) == 0)
+ line += 3;
+
+ /*
+ * Get the count and check it for validity.
+ *
+ * If we have something of the form "1..0 # skip foo", the whole file was
+ * skipped; record that. If we do skip the whole file, zero out all of
+ * our statistics, since they're no longer relevant.
+ *
+ * strtol is called with a second argument to advance the line pointer
+ * past the count to make it simpler to detect the # skip case.
+ */
+ n = strtol(line, (char **) &line, 10);
+ if (n == 0) {
+ line = skip_whitespace(line);
+ if (*line == '#') {
+ line = skip_whitespace(line + 1);
+ if (strncasecmp(line, "skip", 4) == 0) {
+ line = skip_whitespace(line + 4);
+ if (*line != '\0') {
+ ts->reason = xstrdup(line);
+ ts->reason[strlen(ts->reason) - 1] = '\0';
+ }
+ ts->all_skipped = 1;
+ ts->aborted = 1;
+ ts->count = 0;
+ ts->passed = 0;
+ ts->skipped = 0;
+ ts->failed = 0;
+ return 0;
+ }
+ }
+ }
+ if (n <= 0) {
+ puts("ABORTED (invalid test count)");
+ ts->aborted = 1;
+ ts->reported = 1;
+ return 0;
+ }
+
+ /*
+ * If we are doing lazy planning, check the plan against the largest test
+ * number that we saw and fail now if we saw a check outside the plan
+ * range.
+ */
+ if (ts->plan == PLAN_PENDING && (unsigned long) n < ts->count) {
+ invalid_test_number(ts, (long) ts->count, verbose);
+ return 0;
+ }
+
+ /*
+ * Otherwise, allocated or resize the results if needed and update count,
+ * and then record that we've seen a plan.
+ */
+ resize_results(ts, (unsigned long) n);
+ ts->count = (unsigned long) n;
+ if (ts->plan == PLAN_INIT)
+ ts->plan = PLAN_FIRST;
+ else if (ts->plan == PLAN_PENDING)
+ ts->plan = PLAN_FINAL;
+ return 1;
+}
+
+
+/*
+ * Given a single line of output from a test, parse it and return the success
+ * status of that test. Anything printed to stdout not matching the form
+ * /^(not )?ok \d+/ is ignored. Sets ts->current to the test number that just
+ * reported status.
+ */
+static void
+test_checkline(const char *line, struct testset *ts, enum test_verbose verbose)
+{
+ enum test_status status = TEST_PASS;
+ const char *bail;
+ char *end;
+ long number;
+ unsigned long current;
+ int outlen;
+
+ /* Before anything, check for a test abort. */
+ bail = strstr(line, "Bail out!");
+ if (bail != NULL) {
+ bail = skip_whitespace(bail + strlen("Bail out!"));
+ if (*bail != '\0') {
+ size_t length;
+
+ length = strlen(bail);
+ if (bail[length - 1] == '\n')
+ length--;
+ if (!verbose)
+ test_backspace(ts);
+ printf("ABORTED (%.*s)\n", (int) length, bail);
+ ts->reported = 1;
+ }
+ ts->aborted = 1;
+ return;
+ }
+
+ /*
+ * If the given line isn't newline-terminated, it was too big for an
+ * fgets(), which means ignore it.
+ */
+ if (line[strlen(line) - 1] != '\n')
+ return;
+
+ /* If the line begins with a hash mark, ignore it. */
+ if (line[0] == '#')
+ return;
+
+ /* If we haven't yet seen a plan, look for one. */
+ if (ts->plan == PLAN_INIT && isdigit((unsigned char) (*line))) {
+ if (!test_plan(line, ts, verbose))
+ return;
+ } else if (strncmp(line, "1..", 3) == 0) {
+ if (ts->plan == PLAN_PENDING) {
+ if (!test_plan(line, ts, verbose))
+ return;
+ } else {
+ if (!verbose)
+ test_backspace(ts);
+ puts("ABORTED (multiple plans)");
+ ts->aborted = 1;
+ ts->reported = 1;
+ return;
+ }
+ }
+
+ /* Parse the line, ignoring something we can't parse. */
+ if (strncmp(line, "not ", 4) == 0) {
+ status = TEST_FAIL;
+ line += 4;
+ }
+ if (strncmp(line, "ok", 2) != 0)
+ return;
+ line = skip_whitespace(line + 2);
+ errno = 0;
+ number = strtol(line, &end, 10);
+ if (errno != 0 || end == line)
+ current = ts->current + 1;
+ else if (number <= 0) {
+ invalid_test_number(ts, number, verbose);
+ return;
+ } else
+ current = (unsigned long) number;
+ if (current > ts->count && ts->plan == PLAN_FIRST) {
+ invalid_test_number(ts, (long) current, verbose);
+ return;
+ }
+
+ /* We have a valid test result. Tweak the results array if needed. */
+ if (ts->plan == PLAN_INIT || ts->plan == PLAN_PENDING) {
+ ts->plan = PLAN_PENDING;
+ resize_results(ts, current);
+ if (current > ts->count)
+ ts->count = current;
+ }
+
+ /*
+ * Handle directives. We should probably do something more interesting
+ * with unexpected passes of todo tests.
+ */
+ while (isdigit((unsigned char) (*line)))
+ line++;
+ line = skip_whitespace(line);
+ if (*line == '#') {
+ line = skip_whitespace(line + 1);
+ if (strncasecmp(line, "skip", 4) == 0)
+ status = TEST_SKIP;
+ if (strncasecmp(line, "todo", 4) == 0)
+ status = (status == TEST_FAIL) ? TEST_SKIP : TEST_FAIL;
+ }
+
+ /* Make sure that the test number is in range and not a duplicate. */
+ if (ts->results[current - 1] != TEST_INVALID) {
+ if (!verbose)
+ test_backspace(ts);
+ printf("ABORTED (duplicate test number %lu)\n", current);
+ ts->aborted = 1;
+ ts->reported = 1;
+ return;
+ }
+
+ /* Good results. Increment our various counters. */
+ switch (status) {
+ case TEST_PASS:
+ ts->passed++;
+ break;
+ case TEST_FAIL:
+ ts->failed++;
+ break;
+ case TEST_SKIP:
+ ts->skipped++;
+ break;
+ case TEST_INVALID:
+ break;
+ }
+ ts->current = current;
+ ts->results[current - 1] = status;
+ if (!verbose && isatty(STDOUT_FILENO)) {
+ test_backspace(ts);
+ if (ts->plan == PLAN_PENDING)
+ outlen = printf("%lu/?", current);
+ else
+ outlen = printf("%lu/%lu", current, ts->count);
+ ts->length = (outlen >= 0) ? (unsigned int) outlen : 0;
+ fflush(stdout);
+ }
+}
+
+
+/*
+ * Print out a range of test numbers, returning the number of characters it
+ * took up. Takes the first number, the last number, the number of characters
+ * already printed on the line, and the limit of number of characters the line
+ * can hold. Add a comma and a space before the range if chars indicates that
+ * something has already been printed on the line, and print ... instead if
+ * chars plus the space needed would go over the limit (use a limit of 0 to
+ * disable this).
+ */
+static unsigned int
+test_print_range(unsigned long first, unsigned long last, unsigned long chars,
+ unsigned int limit)
+{
+ unsigned int needed = 0;
+ unsigned long n;
+
+ for (n = first; n > 0; n /= 10)
+ needed++;
+ if (last > first) {
+ for (n = last; n > 0; n /= 10)
+ needed++;
+ needed++;
+ }
+ if (chars > 0)
+ needed += 2;
+ if (limit > 0 && chars + needed > limit) {
+ needed = 0;
+ if (chars <= limit) {
+ if (chars > 0) {
+ printf(", ");
+ needed += 2;
+ }
+ printf("...");
+ needed += 3;
+ }
+ } else {
+ if (chars > 0)
+ printf(", ");
+ if (last > first)
+ printf("%lu-", first);
+ printf("%lu", last);
+ }
+ return needed;
+}
+
+
+/*
+ * Summarize a single test set. The second argument is 0 if the set exited
+ * cleanly, a positive integer representing the exit status if it exited
+ * with a non-zero status, and a negative integer representing the signal
+ * that terminated it if it was killed by a signal.
+ */
+static void
+test_summarize(struct testset *ts, int status)
+{
+ unsigned long i;
+ unsigned long missing = 0;
+ unsigned long failed = 0;
+ unsigned long first = 0;
+ unsigned long last = 0;
+
+ if (ts->aborted) {
+ fputs("ABORTED", stdout);
+ if (ts->count > 0)
+ printf(" (passed %lu/%lu)", ts->passed, ts->count - ts->skipped);
+ } else {
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_INVALID) {
+ if (missing == 0)
+ fputs("MISSED ", stdout);
+ if (first && i == last)
+ last = i + 1;
+ else {
+ if (first)
+ test_print_range(first, last, missing - 1, 0);
+ missing++;
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first)
+ test_print_range(first, last, missing - 1, 0);
+ first = 0;
+ last = 0;
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_FAIL) {
+ if (missing && !failed)
+ fputs("; ", stdout);
+ if (failed == 0)
+ fputs("FAILED ", stdout);
+ if (first && i == last)
+ last = i + 1;
+ else {
+ if (first)
+ test_print_range(first, last, failed - 1, 0);
+ failed++;
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first)
+ test_print_range(first, last, failed - 1, 0);
+ if (!missing && !failed) {
+ fputs(!status ? "ok" : "dubious", stdout);
+ if (ts->skipped > 0) {
+ if (ts->skipped == 1)
+ printf(" (skipped %lu test)", ts->skipped);
+ else
+ printf(" (skipped %lu tests)", ts->skipped);
+ }
+ }
+ }
+ if (status > 0)
+ printf(" (exit status %d)", status);
+ else if (status < 0)
+ printf(" (killed by signal %d%s)", -status,
+ WCOREDUMP(ts->status) ? ", core dumped" : "");
+ putchar('\n');
+}
+
+
+/*
+ * Given a test set, analyze the results, classify the exit status, handle a
+ * few special error messages, and then pass it along to test_summarize() for
+ * the regular output. Returns true if the test set ran successfully and all
+ * tests passed or were skipped, false otherwise.
+ */
+static int
+test_analyze(struct testset *ts)
+{
+ if (ts->reported)
+ return 0;
+ if (ts->all_skipped) {
+ if (ts->reason == NULL)
+ puts("skipped");
+ else
+ printf("skipped (%s)\n", ts->reason);
+ return 1;
+ } else if (WIFEXITED(ts->status) && WEXITSTATUS(ts->status) != 0) {
+ switch (WEXITSTATUS(ts->status)) {
+ case CHILDERR_DUP:
+ if (!ts->reported)
+ puts("ABORTED (can't dup file descriptors)");
+ break;
+ case CHILDERR_EXEC:
+ if (!ts->reported)
+ puts("ABORTED (execution failed -- not found?)");
+ break;
+ case CHILDERR_STDIN:
+ case CHILDERR_STDERR:
+ if (!ts->reported)
+ puts("ABORTED (can't open /dev/null)");
+ break;
+ default:
+ test_summarize(ts, WEXITSTATUS(ts->status));
+ break;
+ }
+ return 0;
+ } else if (WIFSIGNALED(ts->status)) {
+ test_summarize(ts, -WTERMSIG(ts->status));
+ return 0;
+ } else if (ts->plan != PLAN_FIRST && ts->plan != PLAN_FINAL) {
+ puts("ABORTED (no valid test plan)");
+ ts->aborted = 1;
+ return 0;
+ } else {
+ test_summarize(ts, 0);
+ return (ts->failed == 0);
+ }
+}
+
+
+/*
+ * Runs a single test set, accumulating and then reporting the results.
+ * Returns true if the test set was successfully run and all tests passed,
+ * false otherwise.
+ */
+static int
+test_run(struct testset *ts, enum test_verbose verbose)
+{
+ pid_t testpid, child;
+ int outfd, status;
+ unsigned long i;
+ FILE *output;
+ char buffer[BUFSIZ];
+
+ /* Run the test program. */
+ testpid = test_start(ts->command, &outfd);
+ output = fdopen(outfd, "r");
+ if (!output) {
+ puts("ABORTED");
+ fflush(stdout);
+ sysdie("fdopen failed");
+ }
+
+ /*
+ * Pass each line of output to test_checkline(), and print the line if
+ * verbosity is requested.
+ */
+ while (!ts->aborted && fgets(buffer, sizeof(buffer), output)) {
+ if (verbose)
+ printf("%s", buffer);
+ test_checkline(buffer, ts, verbose);
+ }
+ if (ferror(output) || ts->plan == PLAN_INIT)
+ ts->aborted = 1;
+ if (!verbose)
+ test_backspace(ts);
+
+ /*
+ * Consume the rest of the test output, close the output descriptor,
+ * retrieve the exit status, and pass that information to test_analyze()
+ * for eventual output.
+ */
+ while (fgets(buffer, sizeof(buffer), output))
+ if (verbose)
+ printf("%s", buffer);
+ fclose(output);
+ child = waitpid(testpid, &ts->status, 0);
+ if (child == (pid_t) -1) {
+ if (!ts->reported) {
+ puts("ABORTED");
+ fflush(stdout);
+ }
+ sysdie("waitpid for %u failed", (unsigned int) testpid);
+ }
+ if (ts->all_skipped)
+ ts->aborted = 0;
+ status = test_analyze(ts);
+
+ /* Convert missing tests to failed tests. */
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_INVALID) {
+ ts->failed++;
+ ts->results[i] = TEST_FAIL;
+ status = 0;
+ }
+ }
+ return status;
+}
+
+
+/* Summarize a list of test failures. */
+static void
+test_fail_summary(const struct testlist *fails)
+{
+ struct testset *ts;
+ unsigned int chars;
+ unsigned long i, first, last, total;
+ double failed;
+
+ puts(header);
+
+ /* Failed Set Fail/Total (%) Skip Stat Failing (25)
+ -------------------------- -------------- ---- ---- -------------- */
+ for (; fails; fails = fails->next) {
+ ts = fails->ts;
+ total = ts->count - ts->skipped;
+ failed = (double) ts->failed;
+ printf("%-26.26s %4lu/%-4lu %3.0f%% %4lu ", ts->file, ts->failed,
+ total, total ? (failed * 100.0) / (double) total : 0,
+ ts->skipped);
+ if (WIFEXITED(ts->status))
+ printf("%4d ", WEXITSTATUS(ts->status));
+ else
+ printf(" -- ");
+ if (ts->aborted) {
+ puts("aborted");
+ continue;
+ }
+ chars = 0;
+ first = 0;
+ last = 0;
+ for (i = 0; i < ts->count; i++) {
+ if (ts->results[i] == TEST_FAIL) {
+ if (first != 0 && i == last)
+ last = i + 1;
+ else {
+ if (first != 0)
+ chars += test_print_range(first, last, chars, 19);
+ first = i + 1;
+ last = i + 1;
+ }
+ }
+ }
+ if (first != 0)
+ test_print_range(first, last, chars, 19);
+ putchar('\n');
+ }
+}
+
+
+/*
+ * Check whether a given file path is a valid test. Currently, this checks
+ * whether it is executable and is a regular file. Returns true or false.
+ */
+static int
+is_valid_test(const char *path)
+{
+ struct stat st;
+
+ if (access(path, X_OK) < 0)
+ return 0;
+ if (stat(path, &st) < 0)
+ return 0;
+ if (!S_ISREG(st.st_mode))
+ return 0;
+ return 1;
+}
+
+
+/*
+ * Given the name of a test, a pointer to the testset struct, and the source
+ * and build directories, find the test. We try first relative to the current
+ * directory, then in the build directory (if not NULL), then in the source
+ * directory. In each of those directories, we first try a "-t" extension and
+ * then a ".t" extension. When we find an executable program, we return the
+ * path to that program. If none of those paths are executable, just fill in
+ * the name of the test as is.
+ *
+ * The caller is responsible for freeing the path member of the testset
+ * struct.
+ */
+static char *
+find_test(const char *name, const char *source, const char *build)
+{
+ char *path = NULL;
+ const char *bases[3], *suffix, *base;
+ unsigned int i, j;
+ const char *suffixes[3] = {"-t", ".t", ""};
+
+ /* Possible base directories. */
+ bases[0] = ".";
+ bases[1] = build;
+ bases[2] = source;
+
+ /* Try each suffix with each base. */
+ for (i = 0; i < ARRAY_SIZE(suffixes); i++) {
+ suffix = suffixes[i];
+ for (j = 0; j < ARRAY_SIZE(bases); j++) {
+ base = bases[j];
+ if (base == NULL)
+ continue;
+ path = concat(base, "/", name, suffix, (const char *) 0);
+ if (is_valid_test(path))
+ return path;
+ free(path);
+ path = NULL;
+ }
+ }
+ if (path == NULL)
+ path = xstrdup(name);
+ return path;
+}
+
+
+/*
+ * Parse a single line of a test list and store the test name and command to
+ * execute it in the given testset struct.
+ *
+ * Normally, each line is just the name of the test, which is located in the
+ * test directory and turned into a command to run. However, each line may
+ * have whitespace-separated options, which change the command that's run.
+ * Current supported options are:
+ *
+ * valgrind
+ * Run the test under valgrind if C_TAP_VALGRIND is set. The contents
+ * of that environment variable are taken as the valgrind command (with
+ * options) to run. The command is parsed with a simple split on
+ * whitespace and no quoting is supported.
+ *
+ * libtool
+ * If running under valgrind, use libtool to invoke valgrind. This avoids
+ * running valgrind on the wrapper shell script generated by libtool. If
+ * set, C_TAP_LIBTOOL must be set to the full path to the libtool program
+ * to use to run valgrind and thus the test. Ignored if the test isn't
+ * being run under valgrind.
+ */
+static void
+parse_test_list_line(const char *line, struct testset *ts, const char *source,
+ const char *build)
+{
+ const char *p, *end, *option, *libtool;
+ const char *valgrind = NULL;
+ unsigned int use_libtool = 0;
+ unsigned int use_valgrind = 0;
+ size_t len, i;
+
+ /* Determine the name of the test. */
+ p = skip_non_whitespace(line);
+ ts->file = xstrndup(line, p - line);
+
+ /* Check if any test options are set. */
+ p = skip_whitespace(p);
+ while (*p != '\0') {
+ end = skip_non_whitespace(p);
+ if (strncmp(p, "libtool", end - p) == 0) {
+ use_libtool = 1;
+ } else if (strncmp(p, "valgrind", end - p) == 0) {
+ valgrind = getenv("C_TAP_VALGRIND");
+ use_valgrind = (valgrind != NULL);
+ } else {
+ option = xstrndup(p, end - p);
+ die("unknown test list option %s", option);
+ }
+ p = skip_whitespace(end);
+ }
+
+ /* Construct the argv to run the test. First, find the length. */
+ len = 1;
+ if (use_valgrind && valgrind != NULL) {
+ p = skip_whitespace(valgrind);
+ while (*p != '\0') {
+ len++;
+ p = skip_whitespace(skip_non_whitespace(p));
+ }
+ if (use_libtool)
+ len += 2;
+ }
+
+ /* Now, build the command. */
+ ts->command = xcalloc(len + 1, char *);
+ i = 0;
+ if (use_valgrind && valgrind != NULL) {
+ if (use_libtool) {
+ libtool = getenv("C_TAP_LIBTOOL");
+ if (libtool == NULL)
+ die("valgrind with libtool requested, but C_TAP_LIBTOOL is not"
+ " set");
+ ts->command[i++] = xstrdup(libtool);
+ ts->command[i++] = xstrdup("--mode=execute");
+ }
+ p = skip_whitespace(valgrind);
+ while (*p != '\0') {
+ end = skip_non_whitespace(p);
+ ts->command[i++] = xstrndup(p, end - p);
+ p = skip_whitespace(end);
+ }
+ }
+ if (i != len - 1)
+ die("internal error while constructing command line");
+ ts->command[i++] = find_test(ts->file, source, build);
+ ts->command[i] = NULL;
+}
+
+
+/*
+ * Read a list of tests from a file, returning the list of tests as a struct
+ * testlist, or NULL if there were no tests (such as a file containing only
+ * comments). Reports an error to standard error and exits if the list of
+ * tests cannot be read.
+ */
+static struct testlist *
+read_test_list(const char *filename, const char *source, const char *build)
+{
+ FILE *file;
+ unsigned int line;
+ size_t length;
+ char buffer[BUFSIZ];
+ const char *start;
+ struct testlist *listhead, *current;
+
+ /* Create the initial container list that will hold our results. */
+ listhead = xcalloc(1, struct testlist);
+ current = NULL;
+
+ /*
+ * Open our file of tests to run and read it line by line, creating a new
+ * struct testlist and struct testset for each line.
+ */
+ file = fopen(filename, "r");
+ if (file == NULL)
+ sysdie("can't open %s", filename);
+ line = 0;
+ while (fgets(buffer, sizeof(buffer), file)) {
+ line++;
+ length = strlen(buffer) - 1;
+ if (buffer[length] != '\n') {
+ fprintf(stderr, "%s:%u: line too long\n", filename, line);
+ exit(1);
+ }
+ buffer[length] = '\0';
+
+ /* Skip comments, leading spaces, and blank lines. */
+ start = skip_whitespace(buffer);
+ if (strlen(start) == 0)
+ continue;
+ if (start[0] == '#')
+ continue;
+
+ /* Allocate the new testset structure. */
+ if (current == NULL)
+ current = listhead;
+ else {
+ current->next = xcalloc(1, struct testlist);
+ current = current->next;
+ }
+ current->ts = xcalloc(1, struct testset);
+ current->ts->plan = PLAN_INIT;
+
+ /* Parse the line and store the results in the testset struct. */
+ parse_test_list_line(start, current->ts, source, build);
+ }
+ fclose(file);
+
+ /* If there were no tests, current is still NULL. */
+ if (current == NULL) {
+ free(listhead);
+ return NULL;
+ }
+
+ /* Return the results. */
+ return listhead;
+}
+
+
+/*
+ * Build a list of tests from command line arguments. Takes the argv and argc
+ * representing the command line arguments and returns a newly allocated test
+ * list, or NULL if there were no tests. The caller is responsible for
+ * freeing.
+ */
+static struct testlist *
+build_test_list(char *argv[], int argc, const char *source, const char *build)
+{
+ int i;
+ struct testlist *listhead, *current;
+
+ /* Create the initial container list that will hold our results. */
+ listhead = xcalloc(1, struct testlist);
+ current = NULL;
+
+ /* Walk the list of arguments and create test sets for them. */
+ for (i = 0; i < argc; i++) {
+ if (current == NULL)
+ current = listhead;
+ else {
+ current->next = xcalloc(1, struct testlist);
+ current = current->next;
+ }
+ current->ts = xcalloc(1, struct testset);
+ current->ts->plan = PLAN_INIT;
+ current->ts->file = xstrdup(argv[i]);
+ current->ts->command = xcalloc(2, char *);
+ current->ts->command[0] = find_test(current->ts->file, source, build);
+ current->ts->command[1] = NULL;
+ }
+
+ /* If there were no tests, current is still NULL. */
+ if (current == NULL) {
+ free(listhead);
+ return NULL;
+ }
+
+ /* Return the results. */
+ return listhead;
+}
+
+
+/* Free a struct testset. */
+static void
+free_testset(struct testset *ts)
+{
+ size_t i;
+
+ free(ts->file);
+ for (i = 0; ts->command[i] != NULL; i++)
+ free(ts->command[i]);
+ free(ts->command);
+ free(ts->results);
+ free(ts->reason);
+ free(ts);
+}
+
+
+/*
+ * Run a batch of tests. Takes two additional parameters: the root of the
+ * source directory and the root of the build directory. Test programs will
+ * be first searched for in the current directory, then the build directory,
+ * then the source directory. Returns true iff all tests passed, and always
+ * frees the test list that's passed in.
+ */
+static int
+test_batch(struct testlist *tests, enum test_verbose verbose)
+{
+ size_t length, i;
+ size_t longest = 0;
+ unsigned int count = 0;
+ struct testset *ts;
+ struct timeval start, end;
+ struct rusage stats;
+ struct testlist *failhead = NULL;
+ struct testlist *failtail = NULL;
+ struct testlist *current, *next;
+ int succeeded;
+ unsigned long total = 0;
+ unsigned long passed = 0;
+ unsigned long skipped = 0;
+ unsigned long failed = 0;
+ unsigned long aborted = 0;
+
+ /* Walk the list of tests to find the longest name. */
+ for (current = tests; current != NULL; current = current->next) {
+ length = strlen(current->ts->file);
+ if (length > longest)
+ longest = length;
+ }
+
+ /*
+ * Add two to longest and round up to the nearest tab stop. This is how
+ * wide the column for printing the current test name will be.
+ */
+ longest += 2;
+ if (longest % 8)
+ longest += 8 - (longest % 8);
+
+ /* Start the wall clock timer. */
+ gettimeofday(&start, NULL);
+
+ /* Now, plow through our tests again, running each one. */
+ for (current = tests; current != NULL; current = current->next) {
+ ts = current->ts;
+
+ /* Print out the name of the test file. */
+ fputs(ts->file, stdout);
+ if (verbose)
+ fputs("\n\n", stdout);
+ else
+ for (i = strlen(ts->file); i < longest; i++)
+ putchar('.');
+ if (isatty(STDOUT_FILENO))
+ fflush(stdout);
+
+ /* Run the test. */
+ succeeded = test_run(ts, verbose);
+ fflush(stdout);
+ if (verbose)
+ putchar('\n');
+
+ /* Record cumulative statistics. */
+ aborted += ts->aborted;
+ total += ts->count + ts->all_skipped;
+ passed += ts->passed;
+ skipped += ts->skipped + ts->all_skipped;
+ failed += ts->failed;
+ count++;
+
+ /* If the test fails, we shuffle it over to the fail list. */
+ if (!succeeded) {
+ if (failhead == NULL) {
+ failhead = xcalloc(1, struct testlist);
+ failtail = failhead;
+ } else {
+ failtail->next = xcalloc(1, struct testlist);
+ failtail = failtail->next;
+ }
+ failtail->ts = ts;
+ failtail->next = NULL;
+ }
+ }
+ total -= skipped;
+
+ /* Stop the timer and get our child resource statistics. */
+ gettimeofday(&end, NULL);
+ getrusage(RUSAGE_CHILDREN, &stats);
+
+ /* Summarize the failures and free the failure list. */
+ if (failhead != NULL) {
+ test_fail_summary(failhead);
+ while (failhead != NULL) {
+ next = failhead->next;
+ free(failhead);
+ failhead = next;
+ }
+ }
+
+ /* Free the memory used by the test lists. */
+ while (tests != NULL) {
+ next = tests->next;
+ free_testset(tests->ts);
+ free(tests);
+ tests = next;
+ }
+
+ /* Print out the final test summary. */
+ putchar('\n');
+ if (aborted != 0) {
+ if (aborted == 1)
+ printf("Aborted %lu test set", aborted);
+ else
+ printf("Aborted %lu test sets", aborted);
+ printf(", passed %lu/%lu tests", passed, total);
+ } else if (failed == 0)
+ fputs("All tests successful", stdout);
+ else
+ printf("Failed %lu/%lu tests, %.2f%% okay", failed, total,
+ (double) (total - failed) * 100.0 / (double) total);
+ if (skipped != 0) {
+ if (skipped == 1)
+ printf(", %lu test skipped", skipped);
+ else
+ printf(", %lu tests skipped", skipped);
+ }
+ puts(".");
+ printf("Files=%u, Tests=%lu", count, total);
+ printf(", %.2f seconds", tv_diff(&end, &start));
+ printf(" (%.2f usr + %.2f sys = %.2f CPU)\n", tv_seconds(&stats.ru_utime),
+ tv_seconds(&stats.ru_stime),
+ tv_sum(&stats.ru_utime, &stats.ru_stime));
+ return (failed == 0 && aborted == 0);
+}
+
+
+/*
+ * Run a single test case. This involves just running the test program after
+ * having done the environment setup and finding the test program.
+ */
+static void
+test_single(const char *program, const char *source, const char *build)
+{
+ char *path;
+
+ path = find_test(program, source, build);
+ if (execl(path, path, (char *) 0) == -1)
+ sysdie("cannot exec %s", path);
+}
+
+
+/*
+ * Main routine. Set the C_TAP_SOURCE, C_TAP_BUILD, SOURCE, and BUILD
+ * environment variables and then, given a file listing tests, run each test
+ * listed.
+ */
+int
+main(int argc, char *argv[])
+{
+ int option;
+ int status = 0;
+ int single = 0;
+ enum test_verbose verbose = CONCISE;
+ char *c_tap_source_env = NULL;
+ char *c_tap_build_env = NULL;
+ char *source_env = NULL;
+ char *build_env = NULL;
+ const char *program;
+ const char *shortlist;
+ const char *list = NULL;
+ const char *source = C_TAP_SOURCE;
+ const char *build = C_TAP_BUILD;
+ struct testlist *tests;
+
+ program = argv[0];
+ while ((option = getopt(argc, argv, "b:hl:os:v")) != EOF) {
+ switch (option) {
+ case 'b':
+ build = optarg;
+ break;
+ case 'h':
+ printf(usage_message, program, program, program, usage_extra);
+ exit(0);
+ case 'l':
+ list = optarg;
+ break;
+ case 'o':
+ single = 1;
+ break;
+ case 's':
+ source = optarg;
+ break;
+ case 'v':
+ verbose = VERBOSE;
+ break;
+ default:
+ exit(1);
+ }
+ }
+ argv += optind;
+ argc -= optind;
+ if ((list == NULL && argc < 1) || (list != NULL && argc > 0)) {
+ fprintf(stderr, usage_message, program, program, program, usage_extra);
+ exit(1);
+ }
+
+ /*
+ * If C_TAP_VERBOSE is set in the environment, that also turns on verbose
+ * mode.
+ */
+ if (getenv("C_TAP_VERBOSE") != NULL)
+ verbose = VERBOSE;
+
+ /*
+ * Set C_TAP_SOURCE and C_TAP_BUILD environment variables. Also set
+ * SOURCE and BUILD for backward compatibility, although we're trying to
+ * migrate to the ones with a C_TAP_* prefix.
+ */
+ if (source != NULL) {
+ c_tap_source_env = concat("C_TAP_SOURCE=", source, (const char *) 0);
+ if (putenv(c_tap_source_env) != 0)
+ sysdie("cannot set C_TAP_SOURCE in the environment");
+ source_env = concat("SOURCE=", source, (const char *) 0);
+ if (putenv(source_env) != 0)
+ sysdie("cannot set SOURCE in the environment");
+ }
+ if (build != NULL) {
+ c_tap_build_env = concat("C_TAP_BUILD=", build, (const char *) 0);
+ if (putenv(c_tap_build_env) != 0)
+ sysdie("cannot set C_TAP_BUILD in the environment");
+ build_env = concat("BUILD=", build, (const char *) 0);
+ if (putenv(build_env) != 0)
+ sysdie("cannot set BUILD in the environment");
+ }
+
+ /* Run the tests as instructed. */
+ if (single)
+ test_single(argv[0], source, build);
+ else if (list != NULL) {
+ shortlist = strrchr(list, '/');
+ if (shortlist == NULL)
+ shortlist = list;
+ else
+ shortlist++;
+ printf(banner, shortlist);
+ tests = read_test_list(list, source, build);
+ status = test_batch(tests, verbose) ? 0 : 1;
+ } else {
+ tests = build_test_list(argv, argc, source, build);
+ status = test_batch(tests, verbose) ? 0 : 1;
+ }
+
+ /* For valgrind cleanliness, free all our memory. */
+ if (source_env != NULL) {
+ putenv((char *) "C_TAP_SOURCE=");
+ putenv((char *) "SOURCE=");
+ free(c_tap_source_env);
+ free(source_env);
+ }
+ if (build_env != NULL) {
+ putenv((char *) "C_TAP_BUILD=");
+ putenv((char *) "BUILD=");
+ free(c_tap_build_env);
+ free(build_env);
+ }
+ exit(status);
+}
diff --git a/tests/style/obsolete-strings-t b/tests/style/obsolete-strings-t
new file mode 100755
index 000000000000..430f07219cef
--- /dev/null
+++ b/tests/style/obsolete-strings-t
@@ -0,0 +1,104 @@
+#!/usr/bin/perl
+#
+# Check for obsolete strings in source files.
+#
+# Examine all source files in a distribution for obsolete strings and report
+# on files that fail this check. This catches various transitions I want to
+# do globally in all my packages, like changing my personal URLs to https.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Copyright 2016, 2018-2020 Russ Allbery <eagle@eyrie.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA qw(skip_unless_author);
+use Test::RRA::Automake qw(all_files automake_setup);
+
+use File::Basename qw(basename);
+use Test::More;
+
+# Bad patterns to search for.
+my @BAD_REGEXES = (qr{ http:// \S+ [.]eyrie[.]org }xms);
+my @BAD_STRINGS = qw(rra@stanford.edu RRA_MAINTAINER_TESTS);
+
+# File names to exclude from this check.
+my %EXCLUDE
+ = map { $_ => 1 } qw(NEWS changelog obsolete-strings.t obsolete-strings-t);
+
+# Only run this test for the package author, since it doesn't indicate any
+# user-noticable flaw in the package itself.
+skip_unless_author('Obsolete strings tests');
+
+# Set up Automake testing.
+automake_setup();
+
+# Check a single file for one of the bad patterns.
+#
+# $path - Path to the file
+#
+# Returns: undef
+sub check_file {
+ my ($path) = @_;
+ my $filename = basename($path);
+
+ # Ignore excluded and binary files.
+ return if $EXCLUDE{$filename};
+ return if !-T $path;
+
+ # Scan the file.
+ open(my $fh, '<', $path) or BAIL_OUT("Cannot open $path");
+ while (defined(my $line = <$fh>)) {
+ for my $regex (@BAD_REGEXES) {
+ if ($line =~ $regex) {
+ ok(0, "$path contains $regex");
+ close($fh) or BAIL_OUT("Cannot close $path");
+ return;
+ }
+ }
+ for my $string (@BAD_STRINGS) {
+ if (index($line, $string) != -1) {
+ ok(0, "$path contains $string");
+ close($fh) or BAIL_OUT("Cannot close $path");
+ return;
+ }
+ }
+ }
+ close($fh) or BAIL_OUT("Cannot close $path");
+ ok(1, $path);
+ return;
+}
+
+# Scan every file for any of the bad patterns or strings. We don't declare a
+# plan since we skip a lot of files and don't want to precalculate the file
+# list.
+my @paths = all_files();
+for my $path (@paths) {
+ check_file($path);
+}
+done_testing();
diff --git a/tests/tap/basic.c b/tests/tap/basic.c
new file mode 100644
index 000000000000..b5f42d0211a4
--- /dev/null
+++ b/tests/tap/basic.c
@@ -0,0 +1,1029 @@
+/*
+ * Some utility routines for writing tests.
+ *
+ * Here are a variety of utility routines for writing tests compatible with
+ * the TAP protocol. All routines of the form ok() or is*() take a test
+ * number and some number of appropriate arguments, check to be sure the
+ * results match the expected output using the arguments, and print out
+ * something appropriate for that test number. Other utility routines help in
+ * constructing more complex tests, skipping tests, reporting errors, setting
+ * up the TAP output format, or finding things in the test environment.
+ *
+ * This file is part of C TAP Harness. The current version plus supporting
+ * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2019 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2001-2002, 2004-2008, 2011-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifdef _WIN32
+# include <direct.h>
+#else
+# include <sys/stat.h>
+#endif
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <tests/tap/basic.h>
+
+/* Windows provides mkdir and rmdir under different names. */
+#ifdef _WIN32
+# define mkdir(p, m) _mkdir(p)
+# define rmdir(p) _rmdir(p)
+#endif
+
+/*
+ * The test count. Always contains the number that will be used for the next
+ * test status. This is exported to callers of the library.
+ */
+unsigned long testnum = 1;
+
+/*
+ * Status information stored so that we can give a test summary at the end of
+ * the test case. We store the planned final test and the count of failures.
+ * We can get the highest test count from testnum.
+ */
+static unsigned long _planned = 0;
+static unsigned long _failed = 0;
+
+/*
+ * Store the PID of the process that called plan() and only summarize
+ * results when that process exits, so as to not misreport results in forked
+ * processes.
+ */
+static pid_t _process = 0;
+
+/*
+ * If true, we're doing lazy planning and will print out the plan based on the
+ * last test number at the end of testing.
+ */
+static int _lazy = 0;
+
+/*
+ * If true, the test was aborted by calling bail(). Currently, this is only
+ * used to ensure that we pass a false value to any cleanup functions even if
+ * all tests to that point have passed.
+ */
+static int _aborted = 0;
+
+/*
+ * Registered cleanup functions. These are stored as a linked list and run in
+ * registered order by finish when the test program exits. Each function is
+ * passed a boolean value indicating whether all tests were successful.
+ */
+struct cleanup_func {
+ test_cleanup_func func;
+ test_cleanup_func_with_data func_with_data;
+ void *data;
+ struct cleanup_func *next;
+};
+static struct cleanup_func *cleanup_funcs = NULL;
+
+/*
+ * Registered diag files. Any output found in these files will be printed out
+ * as if it were passed to diag() before any other output we do. This allows
+ * background processes to log to a file and have that output interleaved with
+ * the test output.
+ */
+struct diag_file {
+ char *name;
+ FILE *file;
+ char *buffer;
+ size_t bufsize;
+ struct diag_file *next;
+};
+static struct diag_file *diag_files = NULL;
+
+/*
+ * Print a specified prefix and then the test description. Handles turning
+ * the argument list into a va_args structure suitable for passing to
+ * print_desc, which has to be done in a macro. Assumes that format is the
+ * argument immediately before the variadic arguments.
+ */
+#define PRINT_DESC(prefix, format) \
+ do { \
+ if (format != NULL) { \
+ va_list args; \
+ printf("%s", prefix); \
+ va_start(args, format); \
+ vprintf(format, args); \
+ va_end(args); \
+ } \
+ } while (0)
+
+
+/*
+ * Form a new string by concatenating multiple strings. The arguments must be
+ * terminated by (const char *) 0.
+ *
+ * This function only exists because we can't assume asprintf. We can't
+ * simulate asprintf with snprintf because we're only assuming SUSv3, which
+ * does not require that snprintf with a NULL buffer return the required
+ * length. When those constraints are relaxed, this should be ripped out and
+ * replaced with asprintf or a more trivial replacement with snprintf.
+ */
+static char *
+concat(const char *first, ...)
+{
+ va_list args;
+ char *result;
+ const char *string;
+ size_t offset;
+ size_t length = 0;
+
+ /*
+ * Find the total memory required. Ensure we don't overflow length. See
+ * the comment for breallocarray for why we're using UINT_MAX here.
+ */
+ va_start(args, first);
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ if (length >= UINT_MAX - strlen(string))
+ bail("strings too long in concat");
+ length += strlen(string);
+ }
+ va_end(args);
+ length++;
+
+ /* Create the string. */
+ result = bcalloc_type(length, char);
+ va_start(args, first);
+ offset = 0;
+ for (string = first; string != NULL; string = va_arg(args, const char *)) {
+ memcpy(result + offset, string, strlen(string));
+ offset += strlen(string);
+ }
+ va_end(args);
+ result[offset] = '\0';
+ return result;
+}
+
+
+/*
+ * Helper function for check_diag_files to handle a single line in a diag
+ * file.
+ *
+ * The general scheme here used is as follows: read one line of output. If we
+ * get NULL, check for an error. If there was one, bail out of the test
+ * program; otherwise, return, and the enclosing loop will check for EOF.
+ *
+ * If we get some data, see if it ends in a newline. If it doesn't end in a
+ * newline, we have one of two cases: our buffer isn't large enough, in which
+ * case we resize it and try again, or we have incomplete data in the file, in
+ * which case we rewind the file and will try again next time.
+ *
+ * Returns a boolean indicating whether the last line was incomplete.
+ */
+static int
+handle_diag_file_line(struct diag_file *file, fpos_t where)
+{
+ int size;
+ size_t length;
+
+ /* Read the next line from the file. */
+ size = file->bufsize > INT_MAX ? INT_MAX : (int) file->bufsize;
+ if (fgets(file->buffer, size, file->file) == NULL) {
+ if (ferror(file->file))
+ sysbail("cannot read from %s", file->name);
+ return 0;
+ }
+
+ /*
+ * See if the line ends in a newline. If not, see which error case we
+ * have.
+ */
+ length = strlen(file->buffer);
+ if (file->buffer[length - 1] != '\n') {
+ int incomplete = 0;
+
+ /* Check whether we ran out of buffer space and resize if so. */
+ if (length < file->bufsize - 1)
+ incomplete = 1;
+ else {
+ file->bufsize += BUFSIZ;
+ file->buffer =
+ breallocarray_type(file->buffer, file->bufsize, char);
+ }
+
+ /*
+ * On either incomplete lines or too small of a buffer, rewind
+ * and read the file again (on the next pass, if incomplete).
+ * It's simpler than trying to double-buffer the file.
+ */
+ if (fsetpos(file->file, &where) < 0)
+ sysbail("cannot set position in %s", file->name);
+ return incomplete;
+ }
+
+ /* We saw a complete line. Print it out. */
+ printf("# %s", file->buffer);
+ return 0;
+}
+
+
+/*
+ * Check all registered diag_files for any output. We only print out the
+ * output if we see a complete line; otherwise, we wait for the next newline.
+ */
+static void
+check_diag_files(void)
+{
+ struct diag_file *file;
+ fpos_t where;
+ int incomplete;
+
+ /*
+ * Walk through each file and read each line of output available.
+ */
+ for (file = diag_files; file != NULL; file = file->next) {
+ clearerr(file->file);
+
+ /* Store the current position in case we have to rewind. */
+ if (fgetpos(file->file, &where) < 0)
+ sysbail("cannot get position in %s", file->name);
+
+ /* Continue until we get EOF or an incomplete line of data. */
+ incomplete = 0;
+ while (!feof(file->file) && !incomplete) {
+ incomplete = handle_diag_file_line(file, where);
+ }
+ }
+}
+
+
+/*
+ * Our exit handler. Called on completion of the test to report a summary of
+ * results provided we're still in the original process. This also handles
+ * printing out the plan if we used plan_lazy(), although that's suppressed if
+ * we never ran a test (due to an early bail, for example), and running any
+ * registered cleanup functions.
+ */
+static void
+finish(void)
+{
+ int success, primary;
+ struct cleanup_func *current;
+ unsigned long highest = testnum - 1;
+ struct diag_file *file, *tmp;
+
+ /* Check for pending diag_file output. */
+ check_diag_files();
+
+ /* Free the diag_files. */
+ file = diag_files;
+ while (file != NULL) {
+ tmp = file;
+ file = file->next;
+ fclose(tmp->file);
+ free(tmp->name);
+ free(tmp->buffer);
+ free(tmp);
+ }
+ diag_files = NULL;
+
+ /*
+ * Determine whether all tests were successful, which is needed before
+ * calling cleanup functions since we pass that fact to the functions.
+ */
+ if (_planned == 0 && _lazy)
+ _planned = highest;
+ success = (!_aborted && _planned == highest && _failed == 0);
+
+ /*
+ * If there are any registered cleanup functions, we run those first. We
+ * always run them, even if we didn't run a test. Don't do anything
+ * except free the diag_files and call cleanup functions if we aren't the
+ * primary process (the process in which plan or plan_lazy was called),
+ * and tell the cleanup functions that fact.
+ */
+ primary = (_process == 0 || getpid() == _process);
+ while (cleanup_funcs != NULL) {
+ if (cleanup_funcs->func_with_data) {
+ void *data = cleanup_funcs->data;
+
+ cleanup_funcs->func_with_data(success, primary, data);
+ } else {
+ cleanup_funcs->func(success, primary);
+ }
+ current = cleanup_funcs;
+ cleanup_funcs = cleanup_funcs->next;
+ free(current);
+ }
+ if (!primary)
+ return;
+
+ /* Don't do anything further if we never planned a test. */
+ if (_planned == 0)
+ return;
+
+ /* If we're aborting due to bail, don't print summaries. */
+ if (_aborted)
+ return;
+
+ /* Print out the lazy plan if needed. */
+ fflush(stderr);
+ if (_lazy && _planned > 0)
+ printf("1..%lu\n", _planned);
+
+ /* Print out a summary of the results. */
+ if (_planned > highest)
+ diag("Looks like you planned %lu test%s but only ran %lu", _planned,
+ (_planned > 1 ? "s" : ""), highest);
+ else if (_planned < highest)
+ diag("Looks like you planned %lu test%s but ran %lu extra", _planned,
+ (_planned > 1 ? "s" : ""), highest - _planned);
+ else if (_failed > 0)
+ diag("Looks like you failed %lu test%s of %lu", _failed,
+ (_failed > 1 ? "s" : ""), _planned);
+ else if (_planned != 1)
+ diag("All %lu tests successful or skipped", _planned);
+ else
+ diag("%lu test successful or skipped", _planned);
+}
+
+
+/*
+ * Initialize things. Turns on line buffering on stdout and then prints out
+ * the number of tests in the test suite. We intentionally don't check for
+ * pending diag_file output here, since it should really come after the plan.
+ */
+void
+plan(unsigned long count)
+{
+ if (setvbuf(stdout, NULL, _IOLBF, BUFSIZ) != 0)
+ sysdiag("cannot set stdout to line buffered");
+ fflush(stderr);
+ printf("1..%lu\n", count);
+ testnum = 1;
+ _planned = count;
+ _process = getpid();
+ if (atexit(finish) != 0) {
+ sysdiag("cannot register exit handler");
+ diag("cleanups will not be run");
+ }
+}
+
+
+/*
+ * Initialize things for lazy planning, where we'll automatically print out a
+ * plan at the end of the program. Turns on line buffering on stdout as well.
+ */
+void
+plan_lazy(void)
+{
+ if (setvbuf(stdout, NULL, _IOLBF, BUFSIZ) != 0)
+ sysdiag("cannot set stdout to line buffered");
+ testnum = 1;
+ _process = getpid();
+ _lazy = 1;
+ if (atexit(finish) != 0)
+ sysbail("cannot register exit handler to display plan");
+}
+
+
+/*
+ * Skip the entire test suite and exits. Should be called instead of plan(),
+ * not after it, since it prints out a special plan line. Ignore diag_file
+ * output here, since it's not clear if it's allowed before the plan.
+ */
+void
+skip_all(const char *format, ...)
+{
+ fflush(stderr);
+ printf("1..0 # skip");
+ PRINT_DESC(" ", format);
+ putchar('\n');
+ exit(0);
+}
+
+
+/*
+ * Takes a boolean success value and assumes the test passes if that value
+ * is true and fails if that value is false.
+ */
+int
+ok(int success, const char *format, ...)
+{
+ fflush(stderr);
+ check_diag_files();
+ printf("%sok %lu", success ? "" : "not ", testnum++);
+ if (!success)
+ _failed++;
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Same as ok(), but takes the format arguments as a va_list.
+ */
+int
+okv(int success, const char *format, va_list args)
+{
+ fflush(stderr);
+ check_diag_files();
+ printf("%sok %lu", success ? "" : "not ", testnum++);
+ if (!success)
+ _failed++;
+ if (format != NULL) {
+ printf(" - ");
+ vprintf(format, args);
+ }
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Skip a test.
+ */
+void
+skip(const char *reason, ...)
+{
+ fflush(stderr);
+ check_diag_files();
+ printf("ok %lu # skip", testnum++);
+ PRINT_DESC(" ", reason);
+ putchar('\n');
+}
+
+
+/*
+ * Report the same status on the next count tests.
+ */
+int
+ok_block(unsigned long count, int success, const char *format, ...)
+{
+ unsigned long i;
+
+ fflush(stderr);
+ check_diag_files();
+ for (i = 0; i < count; i++) {
+ printf("%sok %lu", success ? "" : "not ", testnum++);
+ if (!success)
+ _failed++;
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ }
+ return success;
+}
+
+
+/*
+ * Skip the next count tests.
+ */
+void
+skip_block(unsigned long count, const char *reason, ...)
+{
+ unsigned long i;
+
+ fflush(stderr);
+ check_diag_files();
+ for (i = 0; i < count; i++) {
+ printf("ok %lu # skip", testnum++);
+ PRINT_DESC(" ", reason);
+ putchar('\n');
+ }
+}
+
+
+/*
+ * Takes two boolean values and requires the truth value of both match.
+ */
+int
+is_bool(int left, int right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (!!left == !!right);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %s", !!left ? "true" : "false");
+ diag("right: %s", !!right ? "true" : "false");
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes two integer values and requires they match.
+ */
+int
+is_int(long left, long right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (left == right);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %ld", left);
+ diag("right: %ld", right);
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes two strings and requires they match (using strcmp). NULL arguments
+ * are permitted and handled correctly.
+ */
+int
+is_string(const char *left, const char *right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+
+ /* Compare the strings, being careful of NULL. */
+ if (left == NULL)
+ success = (right == NULL);
+ else if (right == NULL)
+ success = 0;
+ else
+ success = (strcmp(left, right) == 0);
+
+ /* Report the results. */
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %s", left == NULL ? "(null)" : left);
+ diag("right: %s", right == NULL ? "(null)" : right);
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes two unsigned longs and requires they match. On failure, reports them
+ * in hex.
+ */
+int
+is_hex(unsigned long left, unsigned long right, const char *format, ...)
+{
+ int success;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (left == right);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ diag(" left: %lx", (unsigned long) left);
+ diag("right: %lx", (unsigned long) right);
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Takes pointers to a regions of memory and requires that len bytes from each
+ * match. Otherwise reports any bytes which didn't match.
+ */
+int
+is_blob(const void *left, const void *right, size_t len, const char *format,
+ ...)
+{
+ int success;
+ size_t i;
+
+ fflush(stderr);
+ check_diag_files();
+ success = (memcmp(left, right, len) == 0);
+ if (success)
+ printf("ok %lu", testnum++);
+ else {
+ const unsigned char *left_c = (const unsigned char *) left;
+ const unsigned char *right_c = (const unsigned char *) right;
+
+ for (i = 0; i < len; i++) {
+ if (left_c[i] != right_c[i])
+ diag("offset %lu: left %02x, right %02x", (unsigned long) i,
+ left_c[i], right_c[i]);
+ }
+ printf("not ok %lu", testnum++);
+ _failed++;
+ }
+ PRINT_DESC(" - ", format);
+ putchar('\n');
+ return success;
+}
+
+
+/*
+ * Bail out with an error.
+ */
+void
+bail(const char *format, ...)
+{
+ va_list args;
+
+ _aborted = 1;
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("Bail out! ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf("\n");
+ exit(255);
+}
+
+
+/*
+ * Bail out with an error, appending strerror(errno).
+ */
+void
+sysbail(const char *format, ...)
+{
+ va_list args;
+ int oerrno = errno;
+
+ _aborted = 1;
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("Bail out! ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf(": %s\n", strerror(oerrno));
+ exit(255);
+}
+
+
+/*
+ * Report a diagnostic to stderr. Always returns 1 to allow embedding in
+ * compound statements.
+ */
+int
+diag(const char *format, ...)
+{
+ va_list args;
+
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("# ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf("\n");
+ return 1;
+}
+
+
+/*
+ * Report a diagnostic to stderr, appending strerror(errno). Always returns 1
+ * to allow embedding in compound statements.
+ */
+int
+sysdiag(const char *format, ...)
+{
+ va_list args;
+ int oerrno = errno;
+
+ fflush(stderr);
+ check_diag_files();
+ fflush(stdout);
+ printf("# ");
+ va_start(args, format);
+ vprintf(format, args);
+ va_end(args);
+ printf(": %s\n", strerror(oerrno));
+ return 1;
+}
+
+
+/*
+ * Register a new file for diag_file processing.
+ */
+void
+diag_file_add(const char *name)
+{
+ struct diag_file *file, *prev;
+
+ file = bcalloc_type(1, struct diag_file);
+ file->name = bstrdup(name);
+ file->file = fopen(file->name, "r");
+ if (file->file == NULL)
+ sysbail("cannot open %s", name);
+ file->buffer = bcalloc_type(BUFSIZ, char);
+ file->bufsize = BUFSIZ;
+ if (diag_files == NULL)
+ diag_files = file;
+ else {
+ for (prev = diag_files; prev->next != NULL; prev = prev->next)
+ ;
+ prev->next = file;
+ }
+}
+
+
+/*
+ * Remove a file from diag_file processing. If the file is not found, do
+ * nothing, since there are some situations where it can be removed twice
+ * (such as if it's removed from a cleanup function, since cleanup functions
+ * are called after freeing all the diag_files).
+ */
+void
+diag_file_remove(const char *name)
+{
+ struct diag_file *file;
+ struct diag_file **prev = &diag_files;
+
+ for (file = diag_files; file != NULL; file = file->next) {
+ if (strcmp(file->name, name) == 0) {
+ *prev = file->next;
+ fclose(file->file);
+ free(file->name);
+ free(file->buffer);
+ free(file);
+ return;
+ }
+ prev = &file->next;
+ }
+}
+
+
+/*
+ * Allocate cleared memory, reporting a fatal error with bail on failure.
+ */
+void *
+bcalloc(size_t n, size_t size)
+{
+ void *p;
+
+ p = calloc(n, size);
+ if (p == NULL)
+ sysbail("failed to calloc %lu", (unsigned long) (n * size));
+ return p;
+}
+
+
+/*
+ * Allocate memory, reporting a fatal error with bail on failure.
+ */
+void *
+bmalloc(size_t size)
+{
+ void *p;
+
+ p = malloc(size);
+ if (p == NULL)
+ sysbail("failed to malloc %lu", (unsigned long) size);
+ return p;
+}
+
+
+/*
+ * Reallocate memory, reporting a fatal error with bail on failure.
+ */
+void *
+brealloc(void *p, size_t size)
+{
+ p = realloc(p, size);
+ if (p == NULL)
+ sysbail("failed to realloc %lu bytes", (unsigned long) size);
+ return p;
+}
+
+
+/*
+ * The same as brealloc, but determine the size by multiplying an element
+ * count by a size, similar to calloc. The multiplication is checked for
+ * integer overflow.
+ *
+ * We should technically use SIZE_MAX here for the overflow check, but
+ * SIZE_MAX is C99 and we're only assuming C89 + SUSv3, which does not
+ * guarantee that it exists. They do guarantee that UINT_MAX exists, and we
+ * can assume that UINT_MAX <= SIZE_MAX.
+ *
+ * (In theory, C89 and C99 permit size_t to be smaller than unsigned int, but
+ * I disbelieve in the existence of such systems and they will have to cope
+ * without overflow checks.)
+ */
+void *
+breallocarray(void *p, size_t n, size_t size)
+{
+ if (n > 0 && UINT_MAX / n <= size)
+ bail("reallocarray too large");
+ if (n == 0)
+ n = 1;
+ p = realloc(p, n * size);
+ if (p == NULL)
+ sysbail("failed to realloc %lu bytes", (unsigned long) (n * size));
+ return p;
+}
+
+
+/*
+ * Copy a string, reporting a fatal error with bail on failure.
+ */
+char *
+bstrdup(const char *s)
+{
+ char *p;
+ size_t len;
+
+ len = strlen(s) + 1;
+ p = (char *) malloc(len);
+ if (p == NULL)
+ sysbail("failed to strdup %lu bytes", (unsigned long) len);
+ memcpy(p, s, len);
+ return p;
+}
+
+
+/*
+ * Copy up to n characters of a string, reporting a fatal error with bail on
+ * failure. Don't use the system strndup function, since it may not exist and
+ * the TAP library doesn't assume any portability support.
+ */
+char *
+bstrndup(const char *s, size_t n)
+{
+ const char *p;
+ char *copy;
+ size_t length;
+
+ /* Don't assume that the source string is nul-terminated. */
+ for (p = s; (size_t)(p - s) < n && *p != '\0'; p++)
+ ;
+ length = (size_t)(p - s);
+ copy = (char *) malloc(length + 1);
+ if (copy == NULL)
+ sysbail("failed to strndup %lu bytes", (unsigned long) length);
+ memcpy(copy, s, length);
+ copy[length] = '\0';
+ return copy;
+}
+
+
+/*
+ * Locate a test file. Given the partial path to a file, look under
+ * C_TAP_BUILD and then C_TAP_SOURCE for the file and return the full path to
+ * the file. Returns NULL if the file doesn't exist. A non-NULL return
+ * should be freed with test_file_path_free().
+ */
+char *
+test_file_path(const char *file)
+{
+ char *base;
+ char *path = NULL;
+ const char *envs[] = {"C_TAP_BUILD", "C_TAP_SOURCE", NULL};
+ int i;
+
+ for (i = 0; envs[i] != NULL; i++) {
+ base = getenv(envs[i]);
+ if (base == NULL)
+ continue;
+ path = concat(base, "/", file, (const char *) 0);
+ if (access(path, R_OK) == 0)
+ break;
+ free(path);
+ path = NULL;
+ }
+ return path;
+}
+
+
+/*
+ * Free a path returned from test_file_path(). This function exists primarily
+ * for Windows, where memory must be freed from the same library domain that
+ * it was allocated from.
+ */
+void
+test_file_path_free(char *path)
+{
+ free(path);
+}
+
+
+/*
+ * Create a temporary directory, tmp, under C_TAP_BUILD if set and the current
+ * directory if it does not. Returns the path to the temporary directory in
+ * newly allocated memory, and calls bail on any failure. The return value
+ * should be freed with test_tmpdir_free.
+ *
+ * This function uses sprintf because it attempts to be independent of all
+ * other portability layers. The use immediately after a memory allocation
+ * should be safe without using snprintf or strlcpy/strlcat.
+ */
+char *
+test_tmpdir(void)
+{
+ const char *build;
+ char *path = NULL;
+
+ build = getenv("C_TAP_BUILD");
+ if (build == NULL)
+ build = ".";
+ path = concat(build, "/tmp", (const char *) 0);
+ if (access(path, X_OK) < 0)
+ if (mkdir(path, 0777) < 0)
+ sysbail("error creating temporary directory %s", path);
+ return path;
+}
+
+
+/*
+ * Free a path returned from test_tmpdir() and attempt to remove the
+ * directory. If we can't delete the directory, don't worry; something else
+ * that hasn't yet cleaned up may still be using it.
+ */
+void
+test_tmpdir_free(char *path)
+{
+ if (path != NULL)
+ rmdir(path);
+ free(path);
+}
+
+static void
+register_cleanup(test_cleanup_func func,
+ test_cleanup_func_with_data func_with_data, void *data)
+{
+ struct cleanup_func *cleanup, **last;
+
+ cleanup = bcalloc_type(1, struct cleanup_func);
+ cleanup->func = func;
+ cleanup->func_with_data = func_with_data;
+ cleanup->data = data;
+ cleanup->next = NULL;
+ last = &cleanup_funcs;
+ while (*last != NULL)
+ last = &(*last)->next;
+ *last = cleanup;
+}
+
+/*
+ * Register a cleanup function that is called when testing ends. All such
+ * registered functions will be run by finish.
+ */
+void
+test_cleanup_register(test_cleanup_func func)
+{
+ register_cleanup(func, NULL, NULL);
+}
+
+/*
+ * Same as above, but also allows an opaque pointer to be passed to the cleanup
+ * function.
+ */
+void
+test_cleanup_register_with_data(test_cleanup_func_with_data func, void *data)
+{
+ register_cleanup(NULL, func, data);
+}
diff --git a/tests/tap/basic.h b/tests/tap/basic.h
new file mode 100644
index 000000000000..45f15f2892a7
--- /dev/null
+++ b/tests/tap/basic.h
@@ -0,0 +1,192 @@
+/*
+ * Basic utility routines for the TAP protocol.
+ *
+ * This file is part of C TAP Harness. The current version plus supporting
+ * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2019 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2001-2002, 2004-2008, 2011-2012, 2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_BASIC_H
+#define TAP_BASIC_H 1
+
+#include <stdarg.h> /* va_list */
+#include <stddef.h> /* size_t */
+#include <tests/tap/macros.h>
+
+/*
+ * Used for iterating through arrays. ARRAY_SIZE returns the number of
+ * elements in the array (useful for a < upper bound in a for loop) and
+ * ARRAY_END returns a pointer to the element past the end (ISO C99 makes it
+ * legal to refer to such a pointer as long as it's never dereferenced).
+ */
+#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0]))
+#define ARRAY_END(array) (&(array)[ARRAY_SIZE(array)])
+
+BEGIN_DECLS
+
+/*
+ * The test count. Always contains the number that will be used for the next
+ * test status.
+ */
+extern unsigned long testnum;
+
+/* Print out the number of tests and set standard output to line buffered. */
+void plan(unsigned long count);
+
+/*
+ * Prepare for lazy planning, in which the plan will be printed automatically
+ * at the end of the test program.
+ */
+void plan_lazy(void);
+
+/* Skip the entire test suite. Call instead of plan. */
+void skip_all(const char *format, ...)
+ __attribute__((__noreturn__, __format__(printf, 1, 2)));
+
+/*
+ * Basic reporting functions. The okv() function is the same as ok() but
+ * takes the test description as a va_list to make it easier to reuse the
+ * reporting infrastructure when writing new tests. ok() and okv() return the
+ * value of the success argument.
+ */
+int ok(int success, const char *format, ...)
+ __attribute__((__format__(printf, 2, 3)));
+int okv(int success, const char *format, va_list args)
+ __attribute__((__format__(printf, 2, 0)));
+void skip(const char *reason, ...) __attribute__((__format__(printf, 1, 2)));
+
+/*
+ * Report the same status on, or skip, the next count tests. ok_block()
+ * returns the value of the success argument.
+ */
+int ok_block(unsigned long count, int success, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+void skip_block(unsigned long count, const char *reason, ...)
+ __attribute__((__format__(printf, 2, 3)));
+
+/*
+ * Compare two values. Returns true if the test passes and false if it fails.
+ * is_bool takes an int since the bool type isn't fully portable yet, but
+ * interprets both arguments for their truth value, not for their numeric
+ * value.
+ */
+int is_bool(int, int, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_int(long, long, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_string(const char *, const char *, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_hex(unsigned long, unsigned long, const char *format, ...)
+ __attribute__((__format__(printf, 3, 4)));
+int is_blob(const void *, const void *, size_t, const char *format, ...)
+ __attribute__((__format__(printf, 4, 5)));
+
+/* Bail out with an error. sysbail appends strerror(errno). */
+void bail(const char *format, ...)
+ __attribute__((__noreturn__, __nonnull__, __format__(printf, 1, 2)));
+void sysbail(const char *format, ...)
+ __attribute__((__noreturn__, __nonnull__, __format__(printf, 1, 2)));
+
+/* Report a diagnostic to stderr prefixed with #. */
+int diag(const char *format, ...)
+ __attribute__((__nonnull__, __format__(printf, 1, 2)));
+int sysdiag(const char *format, ...)
+ __attribute__((__nonnull__, __format__(printf, 1, 2)));
+
+/*
+ * Register or unregister a file that contains supplementary diagnostics.
+ * Before any other output, all registered files will be read, line by line,
+ * and each line will be reported as a diagnostic as if it were passed to
+ * diag(). Nul characters are not supported in these files and will result in
+ * truncated output.
+ */
+void diag_file_add(const char *file) __attribute__((__nonnull__));
+void diag_file_remove(const char *file) __attribute__((__nonnull__));
+
+/* Allocate memory, reporting a fatal error with bail on failure. */
+void *bcalloc(size_t, size_t)
+ __attribute__((__alloc_size__(1, 2), __malloc__, __warn_unused_result__));
+void *bmalloc(size_t)
+ __attribute__((__alloc_size__(1), __malloc__, __warn_unused_result__));
+void *breallocarray(void *, size_t, size_t)
+ __attribute__((__alloc_size__(2, 3), __malloc__, __warn_unused_result__));
+void *brealloc(void *, size_t)
+ __attribute__((__alloc_size__(2), __malloc__, __warn_unused_result__));
+char *bstrdup(const char *)
+ __attribute__((__malloc__, __nonnull__, __warn_unused_result__));
+char *bstrndup(const char *, size_t)
+ __attribute__((__malloc__, __nonnull__, __warn_unused_result__));
+
+/*
+ * Macros that cast the return value from b* memory functions, making them
+ * usable in C++ code and providing some additional type safety.
+ */
+#define bcalloc_type(n, type) ((type *) bcalloc((n), sizeof(type)))
+#define breallocarray_type(p, n, type) \
+ ((type *) breallocarray((p), (n), sizeof(type)))
+
+/*
+ * Find a test file under C_TAP_BUILD or C_TAP_SOURCE, returning the full
+ * path. The returned path should be freed with test_file_path_free().
+ */
+char *test_file_path(const char *file)
+ __attribute__((__malloc__, __nonnull__, __warn_unused_result__));
+void test_file_path_free(char *path);
+
+/*
+ * Create a temporary directory relative to C_TAP_BUILD and return the path.
+ * The returned path should be freed with test_tmpdir_free().
+ */
+char *test_tmpdir(void) __attribute__((__malloc__, __warn_unused_result__));
+void test_tmpdir_free(char *path);
+
+/*
+ * Register a cleanup function that is called when testing ends. All such
+ * registered functions will be run during atexit handling (and are therefore
+ * subject to all the same constraints and caveats as atexit functions).
+ *
+ * The function must return void and will be passed two arguments: an int that
+ * will be true if the test completed successfully and false otherwise, and an
+ * int that will be true if the cleanup function is run in the primary process
+ * (the one that called plan or plan_lazy) and false otherwise. If
+ * test_cleanup_register_with_data is used instead, a generic pointer can be
+ * provided and will be passed to the cleanup function as a third argument.
+ *
+ * test_cleanup_register_with_data is the better API and should have been the
+ * only API. test_cleanup_register was an API error preserved for backward
+ * cmpatibility.
+ */
+typedef void (*test_cleanup_func)(int, int);
+typedef void (*test_cleanup_func_with_data)(int, int, void *);
+
+void test_cleanup_register(test_cleanup_func) __attribute__((__nonnull__));
+void test_cleanup_register_with_data(test_cleanup_func_with_data, void *)
+ __attribute__((__nonnull__));
+
+END_DECLS
+
+#endif /* TAP_BASIC_H */
diff --git a/tests/tap/kadmin.c b/tests/tap/kadmin.c
new file mode 100644
index 000000000000..8e70f9d0ec27
--- /dev/null
+++ b/tests/tap/kadmin.c
@@ -0,0 +1,138 @@
+/*
+ * Kerberos test setup requiring the kadmin API.
+ *
+ * This file collects Kerberos test setup functions that use the kadmin API to
+ * put principals into particular configurations for testing. Currently, the
+ * only implemented functionality is to mark a password as expired.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#ifdef HAVE_KADM5CLNT
+# include <portable/kadmin.h>
+# include <portable/krb5.h>
+#endif
+#include <portable/system.h>
+
+#include <time.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/kadmin.h>
+#include <tests/tap/kerberos.h>
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+
+/*
+ * Given the principal to set an expiration on, set that principal to have an
+ * expired password. This requires that the realm admin server be configured
+ * either in DNS (with SRV records) or in krb5.conf (possibly the one
+ * KRB5_CONFIG is pointing to). Authentication is done using the keytab
+ * stored in config/admin-keytab.
+ *
+ * Returns true on success. Returns false if necessary configuration is
+ * missing so that the caller can choose whether to call bail or skip_all. If
+ * the configuration is present but the operation fails, bails.
+ */
+#ifdef HAVE_KADM5CLNT
+bool
+kerberos_expire_password(const char *principal, time_t expires)
+{
+ char *path, *user;
+ const char *realm;
+ krb5_context ctx;
+ krb5_principal admin = NULL;
+ krb5_principal princ = NULL;
+ kadm5_ret_t code;
+ kadm5_config_params params;
+ kadm5_principal_ent_rec ent;
+ void *handle;
+ bool okay = false;
+
+ /* Set up for making our call. */
+ path = test_file_path("config/admin-keytab");
+ if (path == NULL)
+ return false;
+ code = krb5_init_context(&ctx);
+ if (code != 0)
+ bail_krb5(ctx, code, "error initializing Kerberos");
+ admin = kerberos_keytab_principal(ctx, path);
+ realm = krb5_principal_get_realm(ctx, admin);
+ code = krb5_set_default_realm(ctx, realm);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot set default realm");
+ code = krb5_unparse_name(ctx, admin, &user);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot unparse admin principal");
+ code = krb5_parse_name(ctx, principal, &princ);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot parse principal %s", principal);
+
+ /*
+ * If the actual kadmin calls fail, we may be built with MIT Kerberos
+ * against a Heimdal server or vice versa. Return false to skip the
+ * tests.
+ */
+ memset(&params, 0, sizeof(params));
+ params.realm = (char *) realm;
+ params.mask = KADM5_CONFIG_REALM;
+ code = kadm5_init_with_skey_ctx(ctx, user, path, KADM5_ADMIN_SERVICE,
+ &params, KADM5_STRUCT_VERSION,
+ KADM5_API_VERSION, &handle);
+ if (code != 0) {
+ diag_krb5(ctx, code, "error initializing kadmin");
+ goto done;
+ }
+ memset(&ent, 0, sizeof(ent));
+ ent.principal = princ;
+ ent.pw_expiration = (krb5_timestamp) expires;
+ code = kadm5_modify_principal(handle, &ent, KADM5_PW_EXPIRATION);
+ if (code == 0)
+ okay = true;
+ else
+ diag_krb5(ctx, code, "error setting password expiration");
+
+done:
+ kadm5_destroy(handle);
+ krb5_free_unparsed_name(ctx, user);
+ krb5_free_principal(ctx, admin);
+ krb5_free_principal(ctx, princ);
+ krb5_free_context(ctx);
+ test_file_path_free(path);
+ return okay;
+}
+#else /* !HAVE_KADM5CLNT */
+bool
+kerberos_expire_password(const char *principal UNUSED, time_t expires UNUSED)
+{
+ return false;
+}
+#endif /* !HAVE_KADM5CLNT */
diff --git a/tests/tap/kadmin.h b/tests/tap/kadmin.h
new file mode 100644
index 000000000000..c4dc657237da
--- /dev/null
+++ b/tests/tap/kadmin.h
@@ -0,0 +1,58 @@
+/*
+ * Utility functions for tests needing Kerberos admin actions.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2011, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_KADMIN_H
+#define TAP_KADMIN_H 1
+
+#include <config.h>
+#include <portable/stdbool.h>
+
+#include <time.h>
+
+#include <tests/tap/macros.h>
+
+BEGIN_DECLS
+
+/*
+ * Given the principal to set an expiration on and the expiration time, set
+ * that principal's key to expire at that time. Authentication is done using
+ * the keytab stored in config/admin-keytab.
+ *
+ * Returns true on success. Returns false if necessary configuration is
+ * missing so that the caller can choose whether to call bail or skip_all. If
+ * the configuration is present but the operation fails, bails.
+ */
+bool kerberos_expire_password(const char *, time_t)
+ __attribute__((__nonnull__));
+
+END_DECLS
+
+#endif /* !TAP_KADMIN_H */
diff --git a/tests/tap/kerberos.c b/tests/tap/kerberos.c
new file mode 100644
index 000000000000..765d80290a64
--- /dev/null
+++ b/tests/tap/kerberos.c
@@ -0,0 +1,544 @@
+/*
+ * Utility functions for tests that use Kerberos.
+ *
+ * The core function is kerberos_setup, which loads Kerberos test
+ * configuration and returns a struct of information. It also supports
+ * obtaining initial tickets from the configured keytab and setting up
+ * KRB5CCNAME and KRB5_KTNAME if a Kerberos keytab is present. Also included
+ * are utility functions for setting up a krb5.conf file and reporting
+ * Kerberos errors or warnings during testing.
+ *
+ * Some of the functionality here is only available if the Kerberos libraries
+ * are available.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2007, 2009-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+#include <portable/system.h>
+
+#include <sys/stat.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/kerberos.h>
+#include <tests/tap/macros.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+/*
+ * Disable the requirement that format strings be literals, since it's easier
+ * to handle the possible patterns for kinit commands as an array.
+ */
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ > 2) || defined(__clang__)
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+
+
+/*
+ * These variables hold the allocated configuration struct, the environment to
+ * point to a different Kerberos ticket cache, keytab, and configuration file,
+ * and the temporary directories used. We store them so that we can free them
+ * on exit for cleaner valgrind output, making it easier to find real memory
+ * leaks in the tested programs.
+ */
+static struct kerberos_config *config = NULL;
+static char *krb5ccname = NULL;
+static char *krb5_ktname = NULL;
+static char *krb5_config = NULL;
+static char *tmpdir_ticket = NULL;
+static char *tmpdir_conf = NULL;
+
+
+/*
+ * Obtain Kerberos tickets and fill in the principal config entry.
+ *
+ * There are two implementations of this function, one if we have native
+ * Kerberos libraries available and one if we don't. Uses keytab to obtain
+ * credentials, and fills in the cache member of the provided config struct.
+ */
+#ifdef HAVE_KRB5
+
+static void
+kerberos_kinit(void)
+{
+ char *name, *krbtgt;
+ krb5_error_code code;
+ krb5_context ctx;
+ krb5_ccache ccache;
+ krb5_principal kprinc;
+ krb5_keytab keytab;
+ krb5_get_init_creds_opt *opts;
+ krb5_creds creds;
+ const char *realm;
+
+ /*
+ * Determine the principal corresponding to that keytab. We copy the
+ * memory to ensure that it's allocated in the right memory domain on
+ * systems where that may matter (like Windows).
+ */
+ code = krb5_init_context(&ctx);
+ if (code != 0)
+ bail_krb5(ctx, code, "error initializing Kerberos");
+ kprinc = kerberos_keytab_principal(ctx, config->keytab);
+ code = krb5_unparse_name(ctx, kprinc, &name);
+ if (code != 0)
+ bail_krb5(ctx, code, "error unparsing name");
+ krb5_free_principal(ctx, kprinc);
+ config->principal = bstrdup(name);
+ krb5_free_unparsed_name(ctx, name);
+
+ /* Now do the Kerberos initialization. */
+ code = krb5_cc_default(ctx, &ccache);
+ if (code != 0)
+ bail_krb5(ctx, code, "error setting ticket cache");
+ code = krb5_parse_name(ctx, config->principal, &kprinc);
+ if (code != 0)
+ bail_krb5(ctx, code, "error parsing principal %s", config->principal);
+ realm = krb5_principal_get_realm(ctx, kprinc);
+ basprintf(&krbtgt, "krbtgt/%s@%s", realm, realm);
+ code = krb5_kt_resolve(ctx, config->keytab, &keytab);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot open keytab %s", config->keytab);
+ code = krb5_get_init_creds_opt_alloc(ctx, &opts);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot allocate credential options");
+ krb5_get_init_creds_opt_set_default_flags(ctx, NULL, realm, opts);
+ krb5_get_init_creds_opt_set_forwardable(opts, 0);
+ krb5_get_init_creds_opt_set_proxiable(opts, 0);
+ code = krb5_get_init_creds_keytab(ctx, &creds, kprinc, keytab, 0, krbtgt,
+ opts);
+ if (code != 0)
+ bail_krb5(ctx, code, "cannot get Kerberos tickets");
+ code = krb5_cc_initialize(ctx, ccache, kprinc);
+ if (code != 0)
+ bail_krb5(ctx, code, "error initializing ticket cache");
+ code = krb5_cc_store_cred(ctx, ccache, &creds);
+ if (code != 0)
+ bail_krb5(ctx, code, "error storing credentials");
+ krb5_cc_close(ctx, ccache);
+ krb5_free_cred_contents(ctx, &creds);
+ krb5_kt_close(ctx, keytab);
+ krb5_free_principal(ctx, kprinc);
+ krb5_get_init_creds_opt_free(ctx, opts);
+ krb5_free_context(ctx);
+ free(krbtgt);
+}
+
+#else /* !HAVE_KRB5 */
+
+static void
+kerberos_kinit(void)
+{
+ static const char *const format[] = {
+ "kinit --no-afslog -k -t %s %s >/dev/null 2>&1 </dev/null",
+ "kinit -k -t %s %s >/dev/null 2>&1 </dev/null",
+ "kinit -t %s %s >/dev/null 2>&1 </dev/null",
+ "kinit -k -K %s %s >/dev/null 2>&1 </dev/null"};
+ FILE *file;
+ char *path;
+ char principal[BUFSIZ], *command;
+ size_t i;
+ int status;
+
+ /* Read the principal corresponding to the keytab. */
+ path = test_file_path("config/principal");
+ if (path == NULL) {
+ test_file_path_free(config->keytab);
+ config->keytab = NULL;
+ return;
+ }
+ file = fopen(path, "r");
+ if (file == NULL) {
+ test_file_path_free(path);
+ return;
+ }
+ test_file_path_free(path);
+ if (fgets(principal, sizeof(principal), file) == NULL)
+ bail("cannot read %s", path);
+ fclose(file);
+ if (principal[strlen(principal) - 1] != '\n')
+ bail("no newline in %s", path);
+ principal[strlen(principal) - 1] = '\0';
+ config->principal = bstrdup(principal);
+
+ /* Now do the Kerberos initialization. */
+ for (i = 0; i < ARRAY_SIZE(format); i++) {
+ basprintf(&command, format[i], config->keytab, principal);
+ status = system(command);
+ free(command);
+ if (status != -1 && WEXITSTATUS(status) == 0)
+ break;
+ }
+ if (status == -1 || WEXITSTATUS(status) != 0)
+ bail("cannot get Kerberos tickets");
+}
+
+#endif /* !HAVE_KRB5 */
+
+
+/*
+ * Free all the memory associated with our Kerberos setup, but don't remove
+ * the ticket cache. This is used when cleaning up on exit from a non-primary
+ * process so that test programs that fork don't remove the ticket cache still
+ * used by the main program.
+ */
+static void
+kerberos_free(void)
+{
+ test_tmpdir_free(tmpdir_ticket);
+ tmpdir_ticket = NULL;
+ if (config != NULL) {
+ test_file_path_free(config->keytab);
+ free(config->principal);
+ free(config->cache);
+ free(config->userprinc);
+ free(config->username);
+ free(config->password);
+ free(config->pkinit_principal);
+ free(config->pkinit_cert);
+ free(config);
+ config = NULL;
+ }
+ if (krb5ccname != NULL) {
+ putenv((char *) "KRB5CCNAME=");
+ free(krb5ccname);
+ krb5ccname = NULL;
+ }
+ if (krb5_ktname != NULL) {
+ putenv((char *) "KRB5_KTNAME=");
+ free(krb5_ktname);
+ krb5_ktname = NULL;
+ }
+}
+
+
+/*
+ * Clean up at the end of a test. This removes the ticket cache and resets
+ * and frees the memory allocated for the environment variables so that
+ * valgrind output on test suites is cleaner. Most of the work is done by
+ * kerberos_free, but this function also deletes the ticket cache.
+ */
+void
+kerberos_cleanup(void)
+{
+ char *path;
+
+ if (tmpdir_ticket != NULL) {
+ basprintf(&path, "%s/krb5cc_test", tmpdir_ticket);
+ unlink(path);
+ free(path);
+ }
+ kerberos_free();
+}
+
+
+/*
+ * The cleanup handler for the TAP framework. Call kerberos_cleanup if we're
+ * in the primary process and kerberos_free if not. The first argument, which
+ * indicates whether the test succeeded or not, is ignored, since we need to
+ * do the same thing either way.
+ */
+static void
+kerberos_cleanup_handler(int success UNUSED, int primary)
+{
+ if (primary)
+ kerberos_cleanup();
+ else
+ kerberos_free();
+}
+
+
+/*
+ * Obtain Kerberos tickets for the principal specified in config/principal
+ * using the keytab specified in config/keytab, both of which are presumed to
+ * be in tests in either the build or the source tree. Also sets KRB5_KTNAME
+ * and KRB5CCNAME.
+ *
+ * Returns the contents of config/principal in newly allocated memory or NULL
+ * if Kerberos tests are apparently not configured. If Kerberos tests are
+ * configured but something else fails, calls bail.
+ */
+struct kerberos_config *
+kerberos_setup(enum kerberos_needs needs)
+{
+ char *path;
+ char buffer[BUFSIZ];
+ FILE *file = NULL;
+
+ /* If we were called before, clean up after the previous run. */
+ if (config != NULL)
+ kerberos_cleanup();
+ config = bcalloc(1, sizeof(struct kerberos_config));
+
+ /*
+ * If we have a config/keytab file, set the KRB5CCNAME and KRB5_KTNAME
+ * environment variables and obtain initial tickets.
+ */
+ config->keytab = test_file_path("config/keytab");
+ if (config->keytab == NULL) {
+ if (needs == TAP_KRB_NEEDS_KEYTAB || needs == TAP_KRB_NEEDS_BOTH)
+ skip_all("Kerberos tests not configured");
+ } else {
+ tmpdir_ticket = test_tmpdir();
+ basprintf(&config->cache, "%s/krb5cc_test", tmpdir_ticket);
+ basprintf(&krb5ccname, "KRB5CCNAME=%s/krb5cc_test", tmpdir_ticket);
+ basprintf(&krb5_ktname, "KRB5_KTNAME=%s", config->keytab);
+ putenv(krb5ccname);
+ putenv(krb5_ktname);
+ kerberos_kinit();
+ }
+
+ /*
+ * If we have a config/password file, read it and fill out the relevant
+ * members of our config struct.
+ */
+ path = test_file_path("config/password");
+ if (path != NULL)
+ file = fopen(path, "r");
+ if (file == NULL) {
+ if (needs == TAP_KRB_NEEDS_PASSWORD || needs == TAP_KRB_NEEDS_BOTH)
+ skip_all("Kerberos tests not configured");
+ } else {
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ bail("cannot read %s", path);
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("no newline in %s", path);
+ buffer[strlen(buffer) - 1] = '\0';
+ config->userprinc = bstrdup(buffer);
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ bail("cannot read password from %s", path);
+ fclose(file);
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("password too long in %s", path);
+ buffer[strlen(buffer) - 1] = '\0';
+ config->password = bstrdup(buffer);
+
+ /*
+ * Strip the realm from the principal and set realm and username.
+ * This is not strictly correct; it doesn't cope with escaped @-signs
+ * or enterprise names.
+ */
+ config->username = bstrdup(config->userprinc);
+ config->realm = strchr(config->username, '@');
+ if (config->realm == NULL)
+ bail("test principal has no realm");
+ *config->realm = '\0';
+ config->realm++;
+ }
+ test_file_path_free(path);
+
+ /*
+ * If we have PKINIT configuration, read it and fill out the relevant
+ * members of our config struct.
+ */
+ path = test_file_path("config/pkinit-principal");
+ if (path != NULL)
+ file = fopen(path, "r");
+ if (path != NULL && file != NULL) {
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ bail("cannot read %s", path);
+ if (buffer[strlen(buffer) - 1] != '\n')
+ bail("no newline in %s", path);
+ buffer[strlen(buffer) - 1] = '\0';
+ fclose(file);
+ test_file_path_free(path);
+ path = test_file_path("config/pkinit-cert");
+ if (path != NULL) {
+ config->pkinit_principal = bstrdup(buffer);
+ config->pkinit_cert = bstrdup(path);
+ }
+ }
+ test_file_path_free(path);
+ if (config->pkinit_cert == NULL && (needs & TAP_KRB_NEEDS_PKINIT) != 0)
+ skip_all("PKINIT tests not configured");
+
+ /*
+ * Register the cleanup function so that the caller doesn't have to do
+ * explicit cleanup.
+ */
+ test_cleanup_register(kerberos_cleanup_handler);
+
+ /* Return the configuration. */
+ return config;
+}
+
+
+/*
+ * Clean up the krb5.conf file generated by kerberos_generate_conf and free
+ * the memory used to set the environment variable. This doesn't fail if the
+ * file and variable are already gone, allowing it to be harmlessly run
+ * multiple times.
+ *
+ * Normally called via an atexit handler.
+ */
+void
+kerberos_cleanup_conf(void)
+{
+ char *path;
+
+ if (tmpdir_conf != NULL) {
+ basprintf(&path, "%s/krb5.conf", tmpdir_conf);
+ unlink(path);
+ free(path);
+ test_tmpdir_free(tmpdir_conf);
+ tmpdir_conf = NULL;
+ }
+ putenv((char *) "KRB5_CONFIG=");
+ free(krb5_config);
+ krb5_config = NULL;
+}
+
+
+/*
+ * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it.
+ * The [appdefaults] section will be stripped out and the default realm will
+ * be set to the realm specified, if not NULL. This will use config/krb5.conf
+ * in preference, so users can configure the tests by creating that file if
+ * the system file isn't suitable.
+ *
+ * Depends on data/generate-krb5-conf being present in the test suite.
+ */
+void
+kerberos_generate_conf(const char *realm)
+{
+ char *path;
+ const char *argv[3];
+
+ if (tmpdir_conf != NULL)
+ kerberos_cleanup_conf();
+ path = test_file_path("data/generate-krb5-conf");
+ if (path == NULL)
+ bail("cannot find generate-krb5-conf");
+ argv[0] = path;
+ argv[1] = realm;
+ argv[2] = NULL;
+ run_setup(argv);
+ test_file_path_free(path);
+ tmpdir_conf = test_tmpdir();
+ basprintf(&krb5_config, "KRB5_CONFIG=%s/krb5.conf", tmpdir_conf);
+ putenv(krb5_config);
+ if (atexit(kerberos_cleanup_conf) != 0)
+ sysdiag("cannot register cleanup function");
+}
+
+
+/*
+ * The remaining functions in this file are only available if Kerberos
+ * libraries are available.
+ */
+#ifdef HAVE_KRB5
+
+
+/*
+ * Report a Kerberos error and bail out. Takes a long instead of a
+ * krb5_error_code because it can also handle a kadm5_ret_t (which may be a
+ * different size).
+ */
+void
+bail_krb5(krb5_context ctx, long code, const char *format, ...)
+{
+ const char *k5_msg = NULL;
+ char *message;
+ va_list args;
+
+ if (ctx != NULL)
+ k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
+ va_start(args, format);
+ bvasprintf(&message, format, args);
+ va_end(args);
+ if (k5_msg == NULL)
+ bail("%s", message);
+ else
+ bail("%s: %s", message, k5_msg);
+}
+
+
+/*
+ * Report a Kerberos error as a diagnostic to stderr. Takes a long instead of
+ * a krb5_error_code because it can also handle a kadm5_ret_t (which may be a
+ * different size).
+ */
+void
+diag_krb5(krb5_context ctx, long code, const char *format, ...)
+{
+ const char *k5_msg = NULL;
+ char *message;
+ va_list args;
+
+ if (ctx != NULL)
+ k5_msg = krb5_get_error_message(ctx, (krb5_error_code) code);
+ va_start(args, format);
+ bvasprintf(&message, format, args);
+ va_end(args);
+ if (k5_msg == NULL)
+ diag("%s", message);
+ else
+ diag("%s: %s", message, k5_msg);
+ free(message);
+ if (k5_msg != NULL)
+ krb5_free_error_message(ctx, k5_msg);
+}
+
+
+/*
+ * Find the principal of the first entry of a keytab and return it. The
+ * caller is responsible for freeing the result with krb5_free_principal.
+ * Exit on error.
+ */
+krb5_principal
+kerberos_keytab_principal(krb5_context ctx, const char *path)
+{
+ krb5_keytab keytab;
+ krb5_kt_cursor cursor;
+ krb5_keytab_entry entry;
+ krb5_principal princ;
+ krb5_error_code status;
+
+ status = krb5_kt_resolve(ctx, path, &keytab);
+ if (status != 0)
+ bail_krb5(ctx, status, "error opening %s", path);
+ status = krb5_kt_start_seq_get(ctx, keytab, &cursor);
+ if (status != 0)
+ bail_krb5(ctx, status, "error reading %s", path);
+ status = krb5_kt_next_entry(ctx, keytab, &entry, &cursor);
+ if (status != 0)
+ bail("no principal found in keytab file %s", path);
+ status = krb5_copy_principal(ctx, entry.principal, &princ);
+ if (status != 0)
+ bail_krb5(ctx, status, "error copying principal from %s", path);
+ krb5_kt_free_entry(ctx, &entry);
+ krb5_kt_end_seq_get(ctx, keytab, &cursor);
+ krb5_kt_close(ctx, keytab);
+ return princ;
+}
+
+#endif /* HAVE_KRB5 */
diff --git a/tests/tap/kerberos.h b/tests/tap/kerberos.h
new file mode 100644
index 000000000000..53dd09619c96
--- /dev/null
+++ b/tests/tap/kerberos.h
@@ -0,0 +1,135 @@
+/*
+ * Utility functions for tests that use Kerberos.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2017, 2020 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2006-2007, 2009, 2011-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_KERBEROS_H
+#define TAP_KERBEROS_H 1
+
+#include <config.h>
+#include <tests/tap/macros.h>
+
+#ifdef HAVE_KRB5
+# include <portable/krb5.h>
+#endif
+
+/* Holds the information parsed from the Kerberos test configuration. */
+struct kerberos_config {
+ char *keytab; /* Path to the keytab. */
+ char *principal; /* Principal whose keys are in the keytab. */
+ char *cache; /* Path to the Kerberos ticket cache. */
+ char *userprinc; /* The fully-qualified principal. */
+ char *username; /* The local (non-realm) part of principal. */
+ char *realm; /* The realm part of the principal. */
+ char *password; /* The password. */
+ char *pkinit_principal; /* Principal for PKINIT authentication. */
+ char *pkinit_cert; /* Path to certificates for PKINIT. */
+};
+
+/*
+ * Whether to skip all tests (by calling skip_all) in kerberos_setup if
+ * certain configuration information isn't available. "_BOTH" means that the
+ * tests require both keytab and password, but PKINIT is not required.
+ */
+enum kerberos_needs
+{
+ /* clang-format off */
+ TAP_KRB_NEEDS_NONE = 0x00,
+ TAP_KRB_NEEDS_KEYTAB = 0x01,
+ TAP_KRB_NEEDS_PASSWORD = 0x02,
+ TAP_KRB_NEEDS_BOTH = 0x01 | 0x02,
+ TAP_KRB_NEEDS_PKINIT = 0x04
+ /* clang-format on */
+};
+
+BEGIN_DECLS
+
+/*
+ * Set up Kerberos, returning the test configuration information. This
+ * obtains Kerberos tickets from config/keytab, if one is present, and stores
+ * them in a Kerberos ticket cache, sets KRB5_KTNAME and KRB5CCNAME. It also
+ * loads the principal and password from config/password, if it exists, and
+ * stores the principal, password, username, and realm in the returned struct.
+ *
+ * If there is no config/keytab file, KRB5_KTNAME and KRB5CCNAME won't be set
+ * and the keytab field will be NULL. If there is no config/password file,
+ * the principal field will be NULL. If the files exist but loading them
+ * fails, or authentication fails, kerberos_setup calls bail.
+ *
+ * kerberos_cleanup will be run as a cleanup function normally, freeing all
+ * resources and cleaning up temporary files on process exit. It can,
+ * however, be called directly if for some reason the caller needs to delete
+ * the Kerberos environment again. However, normally the caller can just call
+ * kerberos_setup again.
+ */
+struct kerberos_config *kerberos_setup(enum kerberos_needs)
+ __attribute__((__malloc__));
+void kerberos_cleanup(void);
+
+/*
+ * Generate a krb5.conf file for testing and set KRB5_CONFIG to point to it.
+ * The [appdefaults] section will be stripped out and the default realm will
+ * be set to the realm specified, if not NULL. This will use config/krb5.conf
+ * in preference, so users can configure the tests by creating that file if
+ * the system file isn't suitable.
+ *
+ * Depends on data/generate-krb5-conf being present in the test suite.
+ *
+ * kerberos_cleanup_conf will clean up after this function, but usually
+ * doesn't need to be called directly since it's registered as an atexit
+ * handler.
+ */
+void kerberos_generate_conf(const char *realm);
+void kerberos_cleanup_conf(void);
+
+/* These interfaces are only available with native Kerberos support. */
+#ifdef HAVE_KRB5
+
+/* Bail out with an error, appending the Kerberos error message. */
+void bail_krb5(krb5_context, long, const char *format, ...)
+ __attribute__((__noreturn__, __nonnull__(3), __format__(printf, 3, 4)));
+
+/* Report a diagnostic with Kerberos error to stderr prefixed with #. */
+void diag_krb5(krb5_context, long, const char *format, ...)
+ __attribute__((__nonnull__(3), __format__(printf, 3, 4)));
+
+/*
+ * Given a Kerberos context and the path to a keytab, retrieve the principal
+ * for the first entry in the keytab and return it. Calls bail on failure.
+ * The returned principal should be freed with krb5_free_principal.
+ */
+krb5_principal kerberos_keytab_principal(krb5_context, const char *path)
+ __attribute__((__nonnull__));
+
+#endif /* HAVE_KRB5 */
+
+END_DECLS
+
+#endif /* !TAP_MESSAGES_H */
diff --git a/tests/tap/libtap.sh b/tests/tap/libtap.sh
new file mode 100644
index 000000000000..1827a689e380
--- /dev/null
+++ b/tests/tap/libtap.sh
@@ -0,0 +1,248 @@
+# Shell function library for test cases.
+#
+# Note that while many of the functions in this library could benefit from
+# using "local" to avoid possibly hammering global variables, Solaris /bin/sh
+# doesn't support local and this library aspires to be portable to Solaris
+# Bourne shell. Instead, all private variables are prefixed with "tap_".
+#
+# This file provides a TAP-compatible shell function library useful for
+# writing test cases. It is part of C TAP Harness, which can be found at
+# <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+#
+# Written by Russ Allbery <eagle@eyrie.org>
+# Copyright 2009-2012, 2016 Russ Allbery <eagle@eyrie.org>
+# Copyright 2006-2008, 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+# Print out the number of test cases we expect to run.
+plan () {
+ count=1
+ planned="$1"
+ failed=0
+ echo "1..$1"
+ trap finish 0
+}
+
+# Prepare for lazy planning.
+plan_lazy () {
+ count=1
+ planned=0
+ failed=0
+ trap finish 0
+}
+
+# Report the test status on exit.
+finish () {
+ tap_highest=`expr "$count" - 1`
+ if [ "$planned" = 0 ] ; then
+ echo "1..$tap_highest"
+ planned="$tap_highest"
+ fi
+ tap_looks='# Looks like you'
+ if [ "$planned" -gt 0 ] ; then
+ if [ "$planned" -gt "$tap_highest" ] ; then
+ if [ "$planned" -gt 1 ] ; then
+ echo "$tap_looks planned $planned tests but only ran" \
+ "$tap_highest"
+ else
+ echo "$tap_looks planned $planned test but only ran" \
+ "$tap_highest"
+ fi
+ elif [ "$planned" -lt "$tap_highest" ] ; then
+ tap_extra=`expr "$tap_highest" - "$planned"`
+ if [ "$planned" -gt 1 ] ; then
+ echo "$tap_looks planned $planned tests but ran" \
+ "$tap_extra extra"
+ else
+ echo "$tap_looks planned $planned test but ran" \
+ "$tap_extra extra"
+ fi
+ elif [ "$failed" -gt 0 ] ; then
+ if [ "$failed" -gt 1 ] ; then
+ echo "$tap_looks failed $failed tests of $planned"
+ else
+ echo "$tap_looks failed $failed test of $planned"
+ fi
+ elif [ "$planned" -gt 1 ] ; then
+ echo "# All $planned tests successful or skipped"
+ else
+ echo "# $planned test successful or skipped"
+ fi
+ fi
+}
+
+# Skip the entire test suite. Should be run instead of plan.
+skip_all () {
+ tap_desc="$1"
+ if [ -n "$tap_desc" ] ; then
+ echo "1..0 # skip $tap_desc"
+ else
+ echo "1..0 # skip"
+ fi
+ exit 0
+}
+
+# ok takes a test description and a command to run and prints success if that
+# command is successful, false otherwise. The count starts at 1 and is
+# updated each time ok is printed.
+ok () {
+ tap_desc="$1"
+ if [ -n "$tap_desc" ] ; then
+ tap_desc=" - $tap_desc"
+ fi
+ shift
+ if "$@" ; then
+ echo ok "$count$tap_desc"
+ else
+ echo not ok "$count$tap_desc"
+ failed=`expr $failed + 1`
+ fi
+ count=`expr $count + 1`
+}
+
+# Skip the next test. Takes the reason why the test is skipped.
+skip () {
+ echo "ok $count # skip $*"
+ count=`expr $count + 1`
+}
+
+# Report the same status on a whole set of tests. Takes the count of tests,
+# the description, and then the command to run to determine the status.
+ok_block () {
+ tap_i=$count
+ tap_end=`expr $count + $1`
+ shift
+ while [ "$tap_i" -lt "$tap_end" ] ; do
+ ok "$@"
+ tap_i=`expr $tap_i + 1`
+ done
+}
+
+# Skip a whole set of tests. Takes the count and then the reason for skipping
+# the test.
+skip_block () {
+ tap_i=$count
+ tap_end=`expr $count + $1`
+ shift
+ while [ "$tap_i" -lt "$tap_end" ] ; do
+ skip "$@"
+ tap_i=`expr $tap_i + 1`
+ done
+}
+
+# Portable variant of printf '%s\n' "$*". In the majority of cases, this
+# function is slower than printf, because the latter is often implemented
+# as a builtin command. The value of the variable IFS is ignored.
+#
+# This macro must not be called via backticks inside double quotes, since this
+# will result in bizarre escaping behavior and lots of extra backslashes on
+# Solaris.
+puts () {
+ cat << EOH
+$@
+EOH
+}
+
+# Run a program expected to succeed, and print ok if it does and produces the
+# correct output. Takes the description, expected exit status, the expected
+# output, the command to run, and then any arguments for that command.
+# Standard output and standard error are combined when analyzing the output of
+# the command.
+#
+# If the command may contain system-specific error messages in its output,
+# add strip_colon_error before the command to post-process its output.
+ok_program () {
+ tap_desc="$1"
+ shift
+ tap_w_status="$1"
+ shift
+ tap_w_output="$1"
+ shift
+ tap_output=`"$@" 2>&1`
+ tap_status=$?
+ if [ $tap_status = $tap_w_status ] \
+ && [ x"$tap_output" = x"$tap_w_output" ] ; then
+ ok "$tap_desc" true
+ else
+ echo "# saw: ($tap_status) $tap_output"
+ echo "# not: ($tap_w_status) $tap_w_output"
+ ok "$tap_desc" false
+ fi
+}
+
+# Strip a colon and everything after it off the output of a command, as long
+# as that colon comes after at least one whitespace character. (This is done
+# to avoid stripping the name of the program from the start of an error
+# message.) This is used to remove system-specific error messages (coming
+# from strerror, for example).
+strip_colon_error() {
+ tap_output=`"$@" 2>&1`
+ tap_status=$?
+ tap_output=`puts "$tap_output" | sed 's/^\([^ ]* [^:]*\):.*/\1/'`
+ puts "$tap_output"
+ return $tap_status
+}
+
+# Bail out with an error message.
+bail () {
+ echo 'Bail out!' "$@"
+ exit 255
+}
+
+# Output a diagnostic on standard error, preceded by the required # mark.
+diag () {
+ echo '#' "$@"
+}
+
+# Search for the given file first in $C_TAP_BUILD and then in $C_TAP_SOURCE
+# and echo the path where the file was found, or the empty string if the file
+# wasn't found.
+#
+# This macro uses puts, so don't run it using backticks inside double quotes
+# or bizarre quoting behavior will happen with Solaris sh.
+test_file_path () {
+ if [ -n "$C_TAP_BUILD" ] && [ -f "$C_TAP_BUILD/$1" ] ; then
+ puts "$C_TAP_BUILD/$1"
+ elif [ -n "$C_TAP_SOURCE" ] && [ -f "$C_TAP_SOURCE/$1" ] ; then
+ puts "$C_TAP_SOURCE/$1"
+ else
+ echo ''
+ fi
+}
+
+# Create $C_TAP_BUILD/tmp for use by tests for storing temporary files and
+# return the path (via standard output).
+#
+# This macro uses puts, so don't run it using backticks inside double quotes
+# or bizarre quoting behavior will happen with Solaris sh.
+test_tmpdir () {
+ if [ -z "$C_TAP_BUILD" ] ; then
+ tap_tmpdir="./tmp"
+ else
+ tap_tmpdir="$C_TAP_BUILD"/tmp
+ fi
+ if [ ! -d "$tap_tmpdir" ] ; then
+ mkdir "$tap_tmpdir" || bail "Error creating $tap_tmpdir"
+ fi
+ puts "$tap_tmpdir"
+}
diff --git a/tests/tap/macros.h b/tests/tap/macros.h
new file mode 100644
index 000000000000..c2c8b5c7315d
--- /dev/null
+++ b/tests/tap/macros.h
@@ -0,0 +1,99 @@
+/*
+ * Helpful macros for TAP header files.
+ *
+ * This is not, strictly speaking, related to TAP, but any TAP add-on is
+ * probably going to need these macros, so define them in one place so that
+ * everyone can pull them in.
+ *
+ * This file is part of C TAP Harness. The current version plus supporting
+ * documentation is at <https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+ *
+ * Copyright 2008, 2012-2013, 2015 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_MACROS_H
+#define TAP_MACROS_H 1
+
+/*
+ * __attribute__ is available in gcc 2.5 and later, but only with gcc 2.7
+ * could you use the __format__ form of the attributes, which is what we use
+ * (to avoid confusion with other macros), and only with gcc 2.96 can you use
+ * the attribute __malloc__. 2.96 is very old, so don't bother trying to get
+ * the other attributes to work with GCC versions between 2.7 and 2.96.
+ */
+#ifndef __attribute__
+# if __GNUC__ < 2 || (__GNUC__ == 2 && __GNUC_MINOR__ < 96)
+# define __attribute__(spec) /* empty */
+# endif
+#endif
+
+/*
+ * We use __alloc_size__, but it was only available in fairly recent versions
+ * of GCC. Suppress warnings about the unknown attribute if GCC is too old.
+ * We know that we're GCC at this point, so we can use the GCC variadic macro
+ * extension, which will still work with versions of GCC too old to have C99
+ * variadic macro support.
+ */
+#if !defined(__attribute__) && !defined(__alloc_size__)
+# if defined(__GNUC__) && !defined(__clang__)
+# if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 3)
+# define __alloc_size__(spec, args...) /* empty */
+# endif
+# endif
+#endif
+
+/* Suppress __warn_unused_result__ if gcc is too old. */
+#if !defined(__attribute__) && !defined(__warn_unused_result__)
+# if __GNUC__ < 3 || (__GNUC__ == 3 && __GNUC_MINOR__ < 4)
+# define __warn_unused_result__ /* empty */
+# endif
+#endif
+
+/*
+ * LLVM and Clang pretend to be GCC but don't support all of the __attribute__
+ * settings that GCC does. For them, suppress warnings about unknown
+ * attributes on declarations. This unfortunately will affect the entire
+ * compilation context, but there's no push and pop available.
+ */
+#if !defined(__attribute__) && (defined(__llvm__) || defined(__clang__))
+# pragma GCC diagnostic ignored "-Wattributes"
+#endif
+
+/* Used for unused parameters to silence gcc warnings. */
+#define UNUSED __attribute__((__unused__))
+
+/*
+ * BEGIN_DECLS is used at the beginning of declarations so that C++
+ * compilers don't mangle their names. END_DECLS is used at the end.
+ */
+#undef BEGIN_DECLS
+#undef END_DECLS
+#ifdef __cplusplus
+# define BEGIN_DECLS extern "C" {
+# define END_DECLS }
+#else
+# define BEGIN_DECLS /* empty */
+# define END_DECLS /* empty */
+#endif
+
+#endif /* TAP_MACROS_H */
diff --git a/tests/tap/perl/Test/RRA.pm b/tests/tap/perl/Test/RRA.pm
new file mode 100644
index 000000000000..6ea65c5701c3
--- /dev/null
+++ b/tests/tap/perl/Test/RRA.pm
@@ -0,0 +1,324 @@
+# Helper functions for test programs written in Perl.
+#
+# This module provides a collection of helper functions used by test programs
+# written in Perl. This is a general collection of functions that can be used
+# by both C packages with Automake and by stand-alone Perl modules. See
+# Test::RRA::Automake for additional functions specifically for C Automake
+# distributions.
+#
+# SPDX-License-Identifier: MIT
+
+package Test::RRA;
+
+use 5.010;
+use base qw(Exporter);
+use strict;
+use warnings;
+
+use Carp qw(croak);
+use File::Temp;
+
+# Abort if Test::More was loaded before Test::RRA to be sure that we get the
+# benefits of the Test::More probing below.
+if ($INC{'Test/More.pm'}) {
+ croak('Test::More loaded before Test::RRA');
+}
+
+# Red Hat's base perl package doesn't include Test::More (one has to install
+# the perl-core package in addition). Try to detect this and skip any Perl
+# tests if Test::More is not present. This relies on Test::RRA being included
+# before Test::More.
+eval {
+ require Test::More;
+ Test::More->import();
+};
+if ($@) {
+ print "1..0 # SKIP Test::More required for test\n"
+ or croak('Cannot write to stdout');
+ exit 0;
+}
+
+# Declare variables that should be set in BEGIN for robustness.
+our (@EXPORT_OK, $VERSION);
+
+# Set $VERSION and everything export-related in a BEGIN block for robustness
+# against circular module loading (not that we load any modules, but
+# consistency is good).
+BEGIN {
+ @EXPORT_OK = qw(
+ is_file_contents skip_unless_author skip_unless_automated use_prereq
+ );
+
+ # This version should match the corresponding rra-c-util release, but with
+ # two digits for the minor version, including a leading zero if necessary,
+ # so that it will sort properly.
+ $VERSION = '10.00';
+}
+
+# Compare a string to the contents of a file, similar to the standard is()
+# function, but to show the line-based unified diff between them if they
+# differ.
+#
+# $got - The output that we received
+# $expected - The path to the file containing the expected output
+# $message - The message to use when reporting the test results
+#
+# Returns: undef
+# Throws: Exception on failure to read or write files or run diff
+sub is_file_contents {
+ my ($got, $expected, $message) = @_;
+
+ # If they're equal, this is simple.
+ open(my $fh, '<', $expected) or BAIL_OUT("Cannot open $expected: $!\n");
+ my $data = do { local $/ = undef; <$fh> };
+ close($fh) or BAIL_OUT("Cannot close $expected: $!\n");
+ if ($got eq $data) {
+ is($got, $data, $message);
+ return;
+ }
+
+ # Otherwise, we show a diff, but only if we have IPC::System::Simple and
+ # diff succeeds. Otherwise, we fall back on showing the full expected and
+ # seen output.
+ eval {
+ require IPC::System::Simple;
+
+ my $tmp = File::Temp->new();
+ my $tmpname = $tmp->filename;
+ print {$tmp} $got or BAIL_OUT("Cannot write to $tmpname: $!\n");
+ my @command = ('diff', '-u', $expected, $tmpname);
+ my $diff = IPC::System::Simple::capturex([0 .. 1], @command);
+ diag($diff);
+ };
+ if ($@) {
+ diag('Expected:');
+ diag($expected);
+ diag('Seen:');
+ diag($data);
+ }
+
+ # Report failure.
+ ok(0, $message);
+ return;
+}
+
+# Skip this test unless author tests are requested. Takes a short description
+# of what tests this script would perform, which is used in the skip message.
+# Calls plan skip_all, which will terminate the program.
+#
+# $description - Short description of the tests
+#
+# Returns: undef
+sub skip_unless_author {
+ my ($description) = @_;
+ if (!$ENV{AUTHOR_TESTING}) {
+ plan(skip_all => "$description only run for author");
+ }
+ return;
+}
+
+# Skip this test unless doing automated testing or release testing. This is
+# used for tests that should be run by CPAN smoke testing or during releases,
+# but not for manual installs by end users. Takes a short description of what
+# tests this script would perform, which is used in the skip message. Calls
+# plan skip_all, which will terminate the program.
+#
+# $description - Short description of the tests
+#
+# Returns: undef
+sub skip_unless_automated {
+ my ($description) = @_;
+ for my $env (qw(AUTOMATED_TESTING RELEASE_TESTING AUTHOR_TESTING)) {
+ return if $ENV{$env};
+ }
+ plan(skip_all => "$description normally skipped");
+ return;
+}
+
+# Attempt to load a module and skip the test if the module could not be
+# loaded. If the module could be loaded, call its import function manually.
+# If the module could not be loaded, calls plan skip_all, which will terminate
+# the program.
+#
+# The special logic here is based on Test::More and is required to get the
+# imports to happen in the caller's namespace.
+#
+# $module - Name of the module to load
+# @imports - Any arguments to import, possibly including a version
+#
+# Returns: undef
+sub use_prereq {
+ my ($module, @imports) = @_;
+
+ # If the first import looks like a version, pass it as a bare string.
+ my $version = q{};
+ if (@imports >= 1 && $imports[0] =~ m{ \A \d+ (?: [.][\d_]+ )* \z }xms) {
+ $version = shift(@imports);
+ }
+
+ # Get caller information to put imports in the correct package.
+ my ($package) = caller;
+
+ # Do the import with eval, and try to isolate it from the surrounding
+ # context as much as possible. Based heavily on Test::More::_eval.
+ ## no critic (BuiltinFunctions::ProhibitStringyEval)
+ ## no critic (ValuesAndExpressions::ProhibitImplicitNewlines)
+ my ($result, $error, $sigdie);
+ {
+ local $@ = undef;
+ local $! = undef;
+ local $SIG{__DIE__} = undef;
+ $result = eval qq{
+ package $package;
+ use $module $version \@imports;
+ 1;
+ };
+ $error = $@;
+ $sigdie = $SIG{__DIE__} || undef;
+ }
+
+ # If the use failed for any reason, skip the test.
+ if (!$result || $error) {
+ my $name = length($version) > 0 ? "$module $version" : $module;
+ plan(skip_all => "$name required for test");
+ }
+
+ # If the module set $SIG{__DIE__}, we cleared that via local. Restore it.
+ ## no critic (Variables::RequireLocalizedPunctuationVars)
+ if (defined($sigdie)) {
+ $SIG{__DIE__} = $sigdie;
+ }
+ return;
+}
+
+1;
+__END__
+
+=for stopwords
+Allbery Allbery's DESC bareword sublicense MERCHANTABILITY NONINFRINGEMENT
+rra-c-util CPAN diff
+
+=head1 NAME
+
+Test::RRA - Support functions for Perl tests
+
+=head1 SYNOPSIS
+
+ use Test::RRA
+ qw(skip_unless_author skip_unless_automated use_prereq);
+
+ # Skip this test unless author tests are requested.
+ skip_unless_author('Coding style tests');
+
+ # Skip this test unless doing automated or release testing.
+ skip_unless_automated('POD syntax tests');
+
+ # Load modules, skipping the test if they're not available.
+ use_prereq('Perl6::Slurp', 'slurp');
+ use_prereq('Test::Script::Run', '0.04');
+
+=head1 DESCRIPTION
+
+This module collects utility functions that are useful for Perl test scripts.
+It assumes Russ Allbery's Perl module layout and test conventions and will
+only be useful for other people if they use the same conventions.
+
+This module B<must> be loaded before Test::More or it will abort during
+import. It will skip the test (by printing a skip message to standard output
+and exiting with status 0, equivalent to C<plan skip_all>) during import if
+Test::More is not available. This allows tests written in Perl using this
+module to be skipped if run on a system with Perl but not Test::More, such as
+Red Hat systems with the C<perl> package but not the C<perl-core> package
+installed.
+
+=head1 FUNCTIONS
+
+None of these functions are imported by default. The ones used by a script
+should be explicitly imported.
+
+=over 4
+
+=item is_file_contents(GOT, EXPECTED, MESSAGE)
+
+Check a string against the contents of a file, showing the differences if any
+using diff (if IPC::System::Simple and diff are available). GOT is the output
+the test received. EXPECTED is the path to a file containing the expected
+output (not the output itself). MESSAGE is a message to display alongside the
+test results.
+
+=item skip_unless_author(DESC)
+
+Checks whether AUTHOR_TESTING is set in the environment and skips the whole
+test (by calling C<plan skip_all> from Test::More) if it is not. DESC is a
+description of the tests being skipped. A space and C<only run for author>
+will be appended to it and used as the skip reason.
+
+=item skip_unless_automated(DESC)
+
+Checks whether AUTHOR_TESTING, AUTOMATED_TESTING, or RELEASE_TESTING are set
+in the environment and skips the whole test (by calling C<plan skip_all> from
+Test::More) if they are not. This should be used by tests that should not run
+during end-user installs of the module, but which should run as part of CPAN
+smoke testing and release testing.
+
+DESC is a description of the tests being skipped. A space and C<normally
+skipped> will be appended to it and used as the skip reason.
+
+=item use_prereq(MODULE[, VERSION][, IMPORT ...])
+
+Attempts to load MODULE with the given VERSION and import arguments. If this
+fails for any reason, the test will be skipped (by calling C<plan skip_all>
+from Test::More) with a skip reason saying that MODULE is required for the
+test.
+
+VERSION will be passed to C<use> as a version bareword if it looks like a
+version number. The remaining IMPORT arguments will be passed as the value of
+an array.
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2016, 2018-2019, 2021 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2013-2014 The Board of Trustees of the Leland Stanford Junior
+University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+Test::More(3), Test::RRA::Automake(3), Test::RRA::Config(3)
+
+This module is maintained in the rra-c-util package. The current version is
+available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+The functions to control when tests are run use environment variables defined
+by the L<Lancaster
+Consensus|https://github.com/Perl-Toolchain-Gang/toolchain-site/blob/master/lancaster-consensus.md>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End:
diff --git a/tests/tap/perl/Test/RRA/Automake.pm b/tests/tap/perl/Test/RRA/Automake.pm
new file mode 100644
index 000000000000..261feab81e27
--- /dev/null
+++ b/tests/tap/perl/Test/RRA/Automake.pm
@@ -0,0 +1,487 @@
+# Helper functions for Perl test programs in Automake distributions.
+#
+# This module provides a collection of helper functions used by test programs
+# written in Perl and included in C source distributions that use Automake.
+# They embed knowledge of how I lay out my source trees and test suites with
+# Autoconf and Automake. They may be usable by others, but doing so will
+# require closely following the conventions implemented by the rra-c-util
+# utility collection.
+#
+# All the functions here assume that C_TAP_BUILD and C_TAP_SOURCE are set in
+# the environment. This is normally done via the C TAP Harness runtests
+# wrapper.
+#
+# SPDX-License-Identifier: MIT
+
+package Test::RRA::Automake;
+
+use 5.010;
+use base qw(Exporter);
+use strict;
+use warnings;
+
+use Exporter;
+use File::Find qw(find);
+use File::Spec;
+use Test::More;
+use Test::RRA::Config qw($LIBRARY_PATH);
+
+# Used below for use lib calls.
+my ($PERL_BLIB_ARCH, $PERL_BLIB_LIB);
+
+# Determine the path to the build tree of any embedded Perl module package in
+# this source package. We do this in a BEGIN block because we're going to use
+# the results in a use lib command below.
+BEGIN {
+ $PERL_BLIB_ARCH = File::Spec->catdir(qw(perl blib arch));
+ $PERL_BLIB_LIB = File::Spec->catdir(qw(perl blib lib));
+
+ # If C_TAP_BUILD is set, we can come up with better values.
+ if (defined($ENV{C_TAP_BUILD})) {
+ my ($vol, $dirs) = File::Spec->splitpath($ENV{C_TAP_BUILD}, 1);
+ my @dirs = File::Spec->splitdir($dirs);
+ pop(@dirs);
+ $PERL_BLIB_ARCH = File::Spec->catdir(@dirs, qw(perl blib arch));
+ $PERL_BLIB_LIB = File::Spec->catdir(@dirs, qw(perl blib lib));
+ }
+}
+
+# Prefer the modules built as part of our source package. Otherwise, we may
+# not find Perl modules while testing, or find the wrong versions.
+use lib $PERL_BLIB_ARCH;
+use lib $PERL_BLIB_LIB;
+
+# Declare variables that should be set in BEGIN for robustness.
+our (@EXPORT_OK, $VERSION);
+
+# Set $VERSION and everything export-related in a BEGIN block for robustness
+# against circular module loading (not that we load any modules, but
+# consistency is good).
+BEGIN {
+ @EXPORT_OK = qw(
+ all_files automake_setup perl_dirs test_file_path test_tmpdir
+ );
+
+ # This version should match the corresponding rra-c-util release, but with
+ # two digits for the minor version, including a leading zero if necessary,
+ # so that it will sort properly.
+ $VERSION = '10.00';
+}
+
+# Directories to skip globally when looking for all files, or for directories
+# that could contain Perl files.
+my @GLOBAL_SKIP = qw(
+ .git .pc _build autom4te.cache build-aux perl/_build perl/blib
+);
+
+# Additional paths to skip when building a list of all files in the
+# distribution. This primarily skips build artifacts that aren't interesting
+# to any of the tests. These match any path component.
+my @FILES_SKIP = qw(
+ .deps .dirstamp .libs aclocal.m4 config.h config.h.in config.h.in~
+ config.log config.status configure configure~
+);
+
+# The temporary directory created by test_tmpdir, if any. If this is set,
+# attempt to remove the directory stored here on program exit (but ignore
+# failure to do so).
+my $TMPDIR;
+
+# Returns a list of all files in the distribution.
+#
+# Returns: List of files
+sub all_files {
+ my @files;
+
+ # Turn the skip lists into hashes for ease of querying.
+ my %skip = map { $_ => 1 } @GLOBAL_SKIP;
+ my %files_skip = map { $_ => 1 } @FILES_SKIP;
+
+ # Wanted function for find. Prune anything matching either of the skip
+ # lists, or *.lo files, and then add all regular files to the list.
+ my $wanted = sub {
+ my $file = $_;
+ my $path = $File::Find::name;
+ $path =~ s{ \A [.]/ }{}xms;
+ if ($skip{$path} || $files_skip{$file} || $file =~ m{ [.]lo\z }xms) {
+ $File::Find::prune = 1;
+ return;
+ }
+ if (!-d $file) {
+ push(@files, $path);
+ }
+ };
+
+ # Do the recursive search and return the results.
+ find($wanted, q{.});
+ return @files;
+}
+
+# Perform initial test setup for running a Perl test in an Automake package.
+# This verifies that C_TAP_BUILD and C_TAP_SOURCE are set and then changes
+# directory to the C_TAP_SOURCE directory by default. Sets LD_LIBRARY_PATH if
+# the $LIBRARY_PATH configuration option is set. Calls BAIL_OUT if
+# C_TAP_BUILD or C_TAP_SOURCE are missing or if anything else fails.
+#
+# $args_ref - Reference to a hash of arguments to configure behavior:
+# chdir_build - If set to a true value, changes to C_TAP_BUILD instead of
+# C_TAP_SOURCE
+#
+# Returns: undef
+sub automake_setup {
+ my ($args_ref) = @_;
+
+ # Bail if C_TAP_BUILD or C_TAP_SOURCE are not set.
+ if (!$ENV{C_TAP_BUILD}) {
+ BAIL_OUT('C_TAP_BUILD not defined (run under runtests)');
+ }
+ if (!$ENV{C_TAP_SOURCE}) {
+ BAIL_OUT('C_TAP_SOURCE not defined (run under runtests)');
+ }
+
+ # C_TAP_BUILD or C_TAP_SOURCE will be the test directory. Change to the
+ # parent.
+ my $start;
+ if ($args_ref->{chdir_build}) {
+ $start = $ENV{C_TAP_BUILD};
+ } else {
+ $start = $ENV{C_TAP_SOURCE};
+ }
+ my ($vol, $dirs) = File::Spec->splitpath($start, 1);
+ my @dirs = File::Spec->splitdir($dirs);
+ pop(@dirs);
+
+ # Simplify relative paths at the end of the directory.
+ my $ups = 0;
+ my $i = $#dirs;
+ while ($i > 2 && $dirs[$i] eq File::Spec->updir) {
+ $ups++;
+ $i--;
+ }
+ for (1 .. $ups) {
+ pop(@dirs);
+ pop(@dirs);
+ }
+ my $root = File::Spec->catpath($vol, File::Spec->catdir(@dirs), q{});
+ chdir($root) or BAIL_OUT("cannot chdir to $root: $!");
+
+ # If C_TAP_BUILD is a subdirectory of C_TAP_SOURCE, add it to the global
+ # ignore list.
+ my ($buildvol, $builddirs) = File::Spec->splitpath($ENV{C_TAP_BUILD}, 1);
+ my @builddirs = File::Spec->splitdir($builddirs);
+ pop(@builddirs);
+ if ($buildvol eq $vol && @builddirs == @dirs + 1) {
+ while (@dirs && $builddirs[0] eq $dirs[0]) {
+ shift(@builddirs);
+ shift(@dirs);
+ }
+ if (@builddirs == 1) {
+ push(@GLOBAL_SKIP, $builddirs[0]);
+ }
+ }
+
+ # Set LD_LIBRARY_PATH if the $LIBRARY_PATH configuration option is set.
+ ## no critic (Variables::RequireLocalizedPunctuationVars)
+ if (defined($LIBRARY_PATH)) {
+ @builddirs = File::Spec->splitdir($builddirs);
+ pop(@builddirs);
+ my $libdir = File::Spec->catdir(@builddirs, $LIBRARY_PATH);
+ my $path = File::Spec->catpath($buildvol, $libdir, q{});
+ if (-d "$path/.libs") {
+ $path .= '/.libs';
+ }
+ if ($ENV{LD_LIBRARY_PATH}) {
+ $ENV{LD_LIBRARY_PATH} .= ":$path";
+ } else {
+ $ENV{LD_LIBRARY_PATH} = $path;
+ }
+ }
+ return;
+}
+
+# Returns a list of directories that may contain Perl scripts and that should
+# be passed to Perl test infrastructure that expects a list of directories to
+# recursively check. The list will be all eligible top-level directories in
+# the package except for the tests directory, which is broken out to one
+# additional level. Calls BAIL_OUT on any problems
+#
+# $args_ref - Reference to a hash of arguments to configure behavior:
+# skip - A reference to an array of directories to skip
+#
+# Returns: List of directories possibly containing Perl scripts to test
+sub perl_dirs {
+ my ($args_ref) = @_;
+
+ # Add the global skip list. We also ignore the perl directory if it
+ # exists since, in my packages, it is treated as a Perl module
+ # distribution and has its own standalone test suite.
+ my @skip = $args_ref->{skip} ? @{ $args_ref->{skip} } : ();
+ push(@skip, @GLOBAL_SKIP, 'perl');
+
+ # Separate directories to skip under tests from top-level directories.
+ my @skip_tests = grep { m{ \A tests/ }xms } @skip;
+ @skip = grep { !m{ \A tests }xms } @skip;
+ for my $skip_dir (@skip_tests) {
+ $skip_dir =~ s{ \A tests/ }{}xms;
+ }
+
+ # Convert the skip lists into hashes for convenience.
+ my %skip = map { $_ => 1 } @skip, 'tests';
+ my %skip_tests = map { $_ => 1 } @skip_tests;
+
+ # Build the list of top-level directories to test.
+ opendir(my $rootdir, q{.}) or BAIL_OUT("cannot open .: $!");
+ my @dirs = grep { -d && !$skip{$_} } readdir($rootdir);
+ closedir($rootdir);
+ @dirs = File::Spec->no_upwards(@dirs);
+
+ # Add the list of subdirectories of the tests directory.
+ if (-d 'tests') {
+ opendir(my $testsdir, q{tests}) or BAIL_OUT("cannot open tests: $!");
+
+ # Skip if found in %skip_tests or if not a directory.
+ my $is_skipped = sub {
+ my ($dir) = @_;
+ return 1 if $skip_tests{$dir};
+ $dir = File::Spec->catdir('tests', $dir);
+ return -d $dir ? 0 : 1;
+ };
+
+ # Build the filtered list of subdirectories of tests.
+ my @test_dirs = grep { !$is_skipped->($_) } readdir($testsdir);
+ closedir($testsdir);
+ @test_dirs = File::Spec->no_upwards(@test_dirs);
+
+ # Add the tests directory to the start of the directory name.
+ push(@dirs, map { File::Spec->catdir('tests', $_) } @test_dirs);
+ }
+ return @dirs;
+}
+
+# Find a configuration file for the test suite. Searches relative to
+# C_TAP_BUILD first and then C_TAP_SOURCE and returns whichever is found
+# first. Calls BAIL_OUT if the file could not be found.
+#
+# $file - Partial path to the file
+#
+# Returns: Full path to the file
+sub test_file_path {
+ my ($file) = @_;
+ BASE:
+ for my $base ($ENV{C_TAP_BUILD}, $ENV{C_TAP_SOURCE}) {
+ next if !defined($base);
+ if (-e "$base/$file") {
+ return "$base/$file";
+ }
+ }
+ BAIL_OUT("cannot find $file");
+ return;
+}
+
+# Create a temporary directory for tests to use for transient files and return
+# the path to that directory. The directory is automatically removed on
+# program exit. The directory permissions use the current umask. Calls
+# BAIL_OUT if the directory could not be created.
+#
+# Returns: Path to a writable temporary directory
+sub test_tmpdir {
+ my $path;
+
+ # If we already figured out what directory to use, reuse the same path.
+ # Otherwise, create a directory relative to C_TAP_BUILD if set.
+ if (defined($TMPDIR)) {
+ $path = $TMPDIR;
+ } else {
+ my $base;
+ if (defined($ENV{C_TAP_BUILD})) {
+ $base = $ENV{C_TAP_BUILD};
+ } else {
+ $base = File::Spec->curdir;
+ }
+ $path = File::Spec->catdir($base, 'tmp');
+ }
+
+ # Create the directory if it doesn't exist.
+ if (!-d $path) {
+ if (!mkdir($path, 0777)) {
+ BAIL_OUT("cannot create directory $path: $!");
+ }
+ }
+
+ # Store the directory name for cleanup and return it.
+ $TMPDIR = $path;
+ return $path;
+}
+
+# On program exit, remove $TMPDIR if set and if possible. Report errors with
+# diag but otherwise ignore them.
+END {
+ if (defined($TMPDIR) && -d $TMPDIR) {
+ local $! = undef;
+ if (!rmdir($TMPDIR)) {
+ diag("cannot remove temporary directory $TMPDIR: $!");
+ }
+ }
+}
+
+1;
+__END__
+
+=for stopwords
+Allbery Automake Automake-aware Automake-based rra-c-util ARGS subdirectories
+sublicense MERCHANTABILITY NONINFRINGEMENT umask
+
+=head1 NAME
+
+Test::RRA::Automake - Automake-aware support functions for Perl tests
+
+=head1 SYNOPSIS
+
+ use Test::RRA::Automake qw(automake_setup perl_dirs test_file_path);
+ automake_setup({ chdir_build => 1 });
+
+ # Paths to directories that may contain Perl scripts.
+ my @dirs = perl_dirs({ skip => [qw(lib)] });
+
+ # Configuration for Kerberos tests.
+ my $keytab = test_file_path('config/keytab');
+
+=head1 DESCRIPTION
+
+This module collects utility functions that are useful for test scripts
+written in Perl and included in a C Automake-based package. They assume the
+layout of a package that uses rra-c-util and C TAP Harness for the test
+structure.
+
+Loading this module will also add the directories C<perl/blib/arch> and
+C<perl/blib/lib> to the Perl library search path, relative to C_TAP_BUILD if
+that environment variable is set. This is harmless for C Automake projects
+that don't contain an embedded Perl module, and for those projects that do,
+this will allow subsequent C<use> calls to find modules that are built as part
+of the package build process.
+
+The automake_setup() function should be called before calling any other
+functions provided by this module.
+
+=head1 FUNCTIONS
+
+None of these functions are imported by default. The ones used by a script
+should be explicitly imported. On failure, all of these functions call
+BAIL_OUT (from Test::More).
+
+=over 4
+
+=item all_files()
+
+Returns a list of all "interesting" files in the distribution that a test
+suite may want to look at. This excludes various products of the build system,
+the build directory if it's under the source directory, and a few other
+uninteresting directories like F<.git>. The returned paths will be paths
+relative to the root of the package.
+
+=item automake_setup([ARGS])
+
+Verifies that the C_TAP_BUILD and C_TAP_SOURCE environment variables are set
+and then changes directory to the top of the source tree (which is one
+directory up from the C_TAP_SOURCE path, since C_TAP_SOURCE points to the top
+of the tests directory).
+
+If ARGS is given, it should be a reference to a hash of configuration options.
+Only one option is supported: C<chdir_build>. If it is set to a true value,
+automake_setup() changes directories to the top of the build tree instead.
+
+=item perl_dirs([ARGS])
+
+Returns a list of directories that may contain Perl scripts that should be
+tested by test scripts that test all Perl in the source tree (such as syntax
+or coding style checks). The paths will be simple directory names relative to
+the current directory or two-part directory names under the F<tests>
+directory. (Directories under F<tests> are broken out separately since it's
+common to want to apply different policies to different subdirectories of
+F<tests>.)
+
+If ARGS is given, it should be a reference to a hash of configuration options.
+Only one option is supported: C<skip>, whose value should be a reference to an
+array of additional top-level directories or directories starting with
+C<tests/> that should be skipped.
+
+=item test_file_path(FILE)
+
+Given FILE, which should be a relative path, locates that file relative to the
+test directory in either the source or build tree. FILE will be checked for
+relative to the environment variable C_TAP_BUILD first, and then relative to
+C_TAP_SOURCE. test_file_path() returns the full path to FILE or calls
+BAIL_OUT if FILE could not be found.
+
+=item test_tmpdir()
+
+Create a temporary directory for tests to use for transient files and return
+the path to that directory. The directory is created relative to the
+C_TAP_BUILD environment variable, which must be set. Permissions on the
+directory are set using the current umask. test_tmpdir() returns the full
+path to the temporary directory or calls BAIL_OUT if it could not be created.
+
+The directory is automatically removed if possible on program exit. Failure
+to remove the directory on exit is reported with diag() and otherwise ignored.
+
+=back
+
+=head1 ENVIRONMENT
+
+=over 4
+
+=item C_TAP_BUILD
+
+The root of the tests directory in Automake build directory for this package,
+used to find files as documented above.
+
+=item C_TAP_SOURCE
+
+The root of the tests directory in the source tree for this package, used to
+find files as documented above.
+
+=back
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2014-2015, 2018-2021 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2013 The Board of Trustees of the Leland Stanford Junior University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+Test::More(3), Test::RRA(3), Test::RRA::Config(3)
+
+This module is maintained in the rra-c-util package. The current version is
+available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+The C TAP Harness test driver and libraries for TAP-based C testing are
+available from L<https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End:
diff --git a/tests/tap/perl/Test/RRA/Config.pm b/tests/tap/perl/Test/RRA/Config.pm
new file mode 100644
index 000000000000..77f967e35b52
--- /dev/null
+++ b/tests/tap/perl/Test/RRA/Config.pm
@@ -0,0 +1,224 @@
+# Configuration for Perl test cases.
+#
+# In order to reuse the same Perl test cases in multiple packages, I use a
+# configuration file to store some package-specific data. This module loads
+# that configuration and provides the namespace for the configuration
+# settings.
+#
+# SPDX-License-Identifier: MIT
+
+package Test::RRA::Config;
+
+use 5.010;
+use base qw(Exporter);
+use strict;
+use warnings;
+
+use Test::More;
+
+# Declare variables that should be set in BEGIN for robustness.
+our (@EXPORT_OK, $VERSION);
+
+# Set $VERSION and everything export-related in a BEGIN block for robustness
+# against circular module loading (not that we load any modules, but
+# consistency is good).
+BEGIN {
+ @EXPORT_OK = qw(
+ $COVERAGE_LEVEL @COVERAGE_SKIP_TESTS @CRITIC_IGNORE $LIBRARY_PATH
+ $MINIMUM_VERSION %MINIMUM_VERSION @MODULE_VERSION_IGNORE
+ @POD_COVERAGE_EXCLUDE @STRICT_IGNORE @STRICT_PREREQ
+ );
+
+ # This version should match the corresponding rra-c-util release, but with
+ # two digits for the minor version, including a leading zero if necessary,
+ # so that it will sort properly.
+ $VERSION = '10.00';
+}
+
+# If C_TAP_BUILD or C_TAP_SOURCE are set in the environment, look for
+# data/perl.conf under those paths for a C Automake package. Otherwise, look
+# in t/data/perl.conf for a standalone Perl module or tests/data/perl.conf for
+# Perl tests embedded in a larger distribution. Don't use Test::RRA::Automake
+# since it may not exist.
+our $PATH;
+for my $base ($ENV{C_TAP_BUILD}, $ENV{C_TAP_SOURCE}, './t', './tests') {
+ next if !defined($base);
+ my $path = "$base/data/perl.conf";
+ if (-r $path) {
+ $PATH = $path;
+ last;
+ }
+}
+if (!defined($PATH)) {
+ BAIL_OUT('cannot find data/perl.conf');
+}
+
+# Pre-declare all of our variables and set any defaults.
+our $COVERAGE_LEVEL = 100;
+our @COVERAGE_SKIP_TESTS;
+our @CRITIC_IGNORE;
+our $LIBRARY_PATH;
+our $MINIMUM_VERSION = '5.010';
+our %MINIMUM_VERSION;
+our @MODULE_VERSION_IGNORE;
+our @POD_COVERAGE_EXCLUDE;
+our @STRICT_IGNORE;
+our @STRICT_PREREQ;
+
+# Load the configuration.
+if (!do($PATH)) {
+ my $error = $@ || $! || 'loading file did not return true';
+ BAIL_OUT("cannot load $PATH: $error");
+}
+
+1;
+__END__
+
+=for stopwords
+Allbery rra-c-util Automake perlcritic .libs namespace subdirectory sublicense
+MERCHANTABILITY NONINFRINGEMENT regexes
+
+=head1 NAME
+
+Test::RRA::Config - Perl test configuration
+
+=head1 SYNOPSIS
+
+ use Test::RRA::Config qw($MINIMUM_VERSION);
+ print "Required Perl version is $MINIMUM_VERSION\n";
+
+=head1 DESCRIPTION
+
+Test::RRA::Config encapsulates per-package configuration for generic Perl test
+programs that are shared between multiple packages using the rra-c-util
+infrastructure. It handles locating and loading the test configuration file
+for both C Automake packages and stand-alone Perl modules.
+
+Test::RRA::Config looks for a file named F<data/perl.conf> relative to the
+root of the test directory. That root is taken from the environment variables
+C_TAP_BUILD or C_TAP_SOURCE (in that order) if set, which will be the case for
+C Automake packages using C TAP Harness. If neither is set, it expects the
+root of the test directory to be a directory named F<t> relative to the
+current directory, which will be the case for stand-alone Perl modules.
+
+The following variables are supported:
+
+=over 4
+
+=item $COVERAGE_LEVEL
+
+The coverage level achieved by the test suite for Perl test coverage testing
+using Test::Strict, as a percentage. The test will fail if test coverage less
+than this percentage is achieved. If not given, defaults to 100.
+
+=item @COVERAGE_SKIP_TESTS
+
+Directories under F<t> whose tests should be skipped when doing coverage
+testing. This can be tests that won't contribute to coverage or tests that
+don't run properly under Devel::Cover for some reason (such as ones that use
+taint checking). F<docs> and F<style> will always be skipped regardless of
+this setting.
+
+=item @CRITIC_IGNORE
+
+Additional files or directories to ignore when doing recursive perlcritic
+testing. To ignore files that will be installed, the path should start with
+F<blib>.
+
+=item $LIBRARY_PATH
+
+Add this directory (or a F<.libs> subdirectory) relative to the top of the
+source tree to LD_LIBRARY_PATH when checking the syntax of Perl modules. This
+may be required to pick up libraries that are used by in-tree Perl modules so
+that Perl scripts can pass a syntax check.
+
+=item $MINIMUM_VERSION
+
+Default minimum version requirement for included Perl scripts. If not given,
+defaults to 5.010.
+
+=item %MINIMUM_VERSION
+
+Minimum version exceptions for specific directories. The keys should be
+minimum versions of Perl to enforce. The value for each key should be a
+reference to an array of either top-level directory names or directory names
+starting with F<tests/>. All files in those directories will have that
+minimum Perl version constraint imposed instead of $MINIMUM_VERSION.
+
+=item @MODULE_VERSION_IGNORE
+
+File names to ignore when checking that all modules in a distribution have the
+same version. Sometimes, some specific modules need separate, special version
+handling, such as modules defining database schemata for DBIx::Class, and
+can't follow the version of the larger package.
+
+=item @POD_COVERAGE_EXCLUDE
+
+Regexes that match method names that should be excluded from POD coverage
+testing. Normally, all methods have to be documented in the POD for a Perl
+module, but methods matching any of these regexes will be considered private
+and won't require documentation.
+
+=item @STRICT_IGNORE
+
+Additional directories to ignore when doing recursive Test::Strict testing for
+C<use strict> and C<use warnings>. The contents of this directory must be
+either top-level directory names or directory names starting with F<tests/>.
+
+=item @STRICT_PREREQ
+
+A list of Perl modules that have to be available in order to do meaningful
+Test::Strict testing. If any of the modules cannot be loaded via C<use>,
+Test::Strict checking will be skipped. There is currently no way to require
+specific versions of the modules.
+
+=back
+
+No variables are exported by default, but the variables can be imported into
+the local namespace to avoid long variable names.
+
+=head1 AUTHOR
+
+Russ Allbery <eagle@eyrie.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2015-2016, 2019, 2021 Russ Allbery <eagle@eyrie.org>
+
+Copyright 2013-2014 The Board of Trustees of the Leland Stanford Junior
+University
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+=head1 SEE ALSO
+
+perlcritic(1), Test::MinimumVersion(3), Test::RRA(3), Test::RRA::Automake(3),
+Test::Strict(3)
+
+This module is maintained in the rra-c-util package. The current version is
+available from L<https://www.eyrie.org/~eagle/software/rra-c-util/>.
+
+The C TAP Harness test driver and libraries for TAP-based C testing are
+available from L<https://www.eyrie.org/~eagle/software/c-tap-harness/>.
+
+=cut
+
+# Local Variables:
+# copyright-at-end-flag: t
+# End:
diff --git a/tests/tap/process.c b/tests/tap/process.c
new file mode 100644
index 000000000000..2f797f8f7567
--- /dev/null
+++ b/tests/tap/process.c
@@ -0,0 +1,532 @@
+/*
+ * Utility functions for tests that use subprocesses.
+ *
+ * Provides utility functions for subprocess manipulation. Specifically,
+ * provides a function, run_setup, which runs a command and bails if it fails,
+ * using its error message as the bail output, and is_function_output, which
+ * runs a function in a subprocess and checks its output and exit status
+ * against expected values.
+ *
+ * Requires an Autoconf probe for sys/select.h and a replacement for a missing
+ * mkstemp.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2002, 2004-2005, 2013, 2016-2017 Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2011, 2013-2014
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#ifdef HAVE_SYS_SELECT_H
+# include <sys/select.h>
+#endif
+#include <sys/stat.h>
+#ifdef HAVE_SYS_TIME_H
+# include <sys/time.h>
+#endif
+#include <sys/wait.h>
+#include <time.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/process.h>
+#include <tests/tap/string.h>
+
+/* May be defined by the build system. */
+#ifndef PATH_FAKEROOT
+# define PATH_FAKEROOT ""
+#endif
+
+/* How long to wait for the process to start in seconds. */
+#define PROCESS_WAIT 10
+
+/*
+ * Used to store information about a background process. This contains
+ * everything required to stop the process and clean up after it.
+ */
+struct process {
+ pid_t pid; /* PID of child process */
+ char *pidfile; /* PID file to delete on process stop */
+ char *tmpdir; /* Temporary directory for log file */
+ char *logfile; /* Log file of process output */
+ bool is_child; /* Whether we can waitpid for process */
+ struct process *next; /* Next process in global list */
+};
+
+/*
+ * Global list of started processes, which will be cleaned up automatically on
+ * program exit if they haven't been explicitly stopped with process_stop
+ * prior to that point.
+ */
+static struct process *processes = NULL;
+
+
+/*
+ * Given a function, an expected exit status, and expected output, runs that
+ * function in a subprocess, capturing stdout and stderr via a pipe, and
+ * returns the function output in newly allocated memory. Also captures the
+ * process exit status.
+ */
+static void
+run_child_function(test_function_type function, void *data, int *status,
+ char **output)
+{
+ int fds[2];
+ pid_t child;
+ char *buf;
+ ssize_t count, ret, buflen;
+ int rval;
+
+ /* Flush stdout before we start to avoid odd forking issues. */
+ fflush(stdout);
+
+ /* Set up the pipe and call the function, collecting its output. */
+ if (pipe(fds) == -1)
+ sysbail("can't create pipe");
+ child = fork();
+ if (child == (pid_t) -1) {
+ sysbail("can't fork");
+ } else if (child == 0) {
+ /* In child. Set up our stdout and stderr. */
+ close(fds[0]);
+ if (dup2(fds[1], 1) == -1)
+ _exit(255);
+ if (dup2(fds[1], 2) == -1)
+ _exit(255);
+
+ /* Now, run the function and exit successfully if it returns. */
+ (*function)(data);
+ fflush(stdout);
+ _exit(0);
+ } else {
+ /*
+ * In the parent; close the extra file descriptor, read the output if
+ * any, and then collect the exit status.
+ */
+ close(fds[1]);
+ buflen = BUFSIZ;
+ buf = bmalloc(buflen);
+ count = 0;
+ do {
+ ret = read(fds[0], buf + count, buflen - count - 1);
+ if (SSIZE_MAX - count <= ret)
+ bail("maximum output size exceeded in run_child_function");
+ if (ret > 0)
+ count += ret;
+ if (count >= buflen - 1) {
+ buflen += BUFSIZ;
+ buf = brealloc(buf, buflen);
+ }
+ } while (ret > 0);
+ buf[count] = '\0';
+ if (waitpid(child, &rval, 0) == (pid_t) -1)
+ sysbail("waitpid failed");
+ close(fds[0]);
+ }
+
+ /* Store the output and return. */
+ *status = rval;
+ *output = buf;
+}
+
+
+/*
+ * Given a function, data to pass to that function, an expected exit status,
+ * and expected output, runs that function in a subprocess, capturing stdout
+ * and stderr via a pipe, and compare the combination of stdout and stderr
+ * with the expected output and the exit status with the expected status.
+ * Expects the function to always exit (not die from a signal).
+ */
+void
+is_function_output(test_function_type function, void *data, int status,
+ const char *output, const char *format, ...)
+{
+ char *buf, *msg;
+ int rval;
+ va_list args;
+
+ run_child_function(function, data, &rval, &buf);
+
+ /* Now, check the results against what we expected. */
+ va_start(args, format);
+ bvasprintf(&msg, format, args);
+ va_end(args);
+ ok(WIFEXITED(rval), "%s (exited)", msg);
+ is_int(status, WEXITSTATUS(rval), "%s (status)", msg);
+ is_string(output, buf, "%s (output)", msg);
+ free(buf);
+ free(msg);
+}
+
+
+/*
+ * A helper function for run_setup. This is a function to run an external
+ * command, suitable for passing into run_child_function. The expected
+ * argument must be an argv array, with argv[0] being the command to run.
+ */
+static void
+exec_command(void *data)
+{
+ char *const *argv = data;
+
+ execvp(argv[0], argv);
+}
+
+
+/*
+ * Given a command expressed as an argv struct, with argv[0] the name or path
+ * to the command, run that command. If it exits with a non-zero status, use
+ * the part of its output up to the first newline as the error message when
+ * calling bail.
+ */
+void
+run_setup(const char *const argv[])
+{
+ char *output, *p;
+ int status;
+
+ run_child_function(exec_command, (void *) argv, &status, &output);
+ if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
+ p = strchr(output, '\n');
+ if (p != NULL)
+ *p = '\0';
+ if (output[0] != '\0')
+ bail("%s", output);
+ else
+ bail("setup command failed with no output");
+ }
+ free(output);
+}
+
+
+/*
+ * Free the resources associated with tracking a process, without doing
+ * anything to the process. This is kept separate so that we can free
+ * resources during shutdown in a non-primary process.
+ */
+static void
+process_free(struct process *process)
+{
+ struct process **prev;
+
+ /* Do nothing if called with a NULL argument. */
+ if (process == NULL)
+ return;
+
+ /* Remove the process from the global list. */
+ prev = &processes;
+ while (*prev != NULL && *prev != process)
+ prev = &(*prev)->next;
+ if (*prev == process)
+ *prev = process->next;
+
+ /* Free resources. */
+ free(process->pidfile);
+ free(process->logfile);
+ test_tmpdir_free(process->tmpdir);
+ free(process);
+}
+
+
+/*
+ * Kill a process and wait for it to exit. Returns the status of the process.
+ * Calls bail on a system failure or a failure of the process to exit.
+ *
+ * We are quite aggressive with error reporting here because child processes
+ * that don't exit or that don't exist often indicate some form of test
+ * failure.
+ */
+static int
+process_kill(struct process *process)
+{
+ int result, i;
+ int status = -1;
+ struct timeval tv;
+ unsigned long pid = process->pid;
+
+ /* If the process is not a child, just kill it and hope. */
+ if (!process->is_child) {
+ if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH)
+ sysbail("cannot send SIGTERM to process %lu", pid);
+ return 0;
+ }
+
+ /* Check if the process has already exited. */
+ result = waitpid(process->pid, &status, WNOHANG);
+ if (result < 0)
+ sysbail("cannot wait for child process %lu", pid);
+ else if (result > 0)
+ return status;
+
+ /*
+ * Kill the process and wait for it to exit. I don't want to go to the
+ * work of setting up a SIGCHLD handler or a full event loop here, so we
+ * effectively poll every tenth of a second for process exit (and
+ * hopefully faster when it does since the SIGCHLD may interrupt our
+ * select, although we're racing with it.
+ */
+ if (kill(process->pid, SIGTERM) < 0 && errno != ESRCH)
+ sysbail("cannot send SIGTERM to child process %lu", pid);
+ for (i = 0; i < PROCESS_WAIT * 10; i++) {
+ tv.tv_sec = 0;
+ tv.tv_usec = 100000;
+ select(0, NULL, NULL, NULL, &tv);
+ result = waitpid(process->pid, &status, WNOHANG);
+ if (result < 0)
+ sysbail("cannot wait for child process %lu", pid);
+ else if (result > 0)
+ return status;
+ }
+
+ /* The process still hasn't exited. Bail. */
+ bail("child process %lu did not exit on SIGTERM", pid);
+
+ /* Not reached, but some compilers may get confused. */
+ return status;
+}
+
+
+/*
+ * Stop a particular process given its process struct. This kills the
+ * process, waits for it to exit if possible (giving it at most five seconds),
+ * and then removes it from the global processes struct so that it isn't
+ * stopped again during global shutdown.
+ */
+void
+process_stop(struct process *process)
+{
+ int status;
+ unsigned long pid = process->pid;
+
+ /* Stop the process. */
+ status = process_kill(process);
+
+ /* Call diag to flush logs as well as provide exit status. */
+ if (process->is_child)
+ diag("stopped process %lu (exit status %d)", pid, status);
+ else
+ diag("stopped process %lu", pid);
+
+ /* Remove the log and PID file. */
+ diag_file_remove(process->logfile);
+ unlink(process->pidfile);
+ unlink(process->logfile);
+
+ /* Free resources. */
+ process_free(process);
+}
+
+
+/*
+ * Stop all running processes. This is called as a cleanup handler during
+ * process shutdown. The first argument, which says whether the test was
+ * successful, is ignored, since the same actions should be performed
+ * regardless. The second argument says whether this is the primary process,
+ * in which case we do the full shutdown. Otherwise, we only free resources
+ * but don't stop the process.
+ */
+static void
+process_stop_all(int success UNUSED, int primary)
+{
+ while (processes != NULL) {
+ if (primary)
+ process_stop(processes);
+ else
+ process_free(processes);
+ }
+}
+
+
+/*
+ * Read the PID of a process from a file. This is necessary when running
+ * under fakeroot to get the actual PID of the remctld process.
+ */
+static pid_t
+read_pidfile(const char *path)
+{
+ FILE *file;
+ char buffer[BUFSIZ];
+ long pid;
+
+ file = fopen(path, "r");
+ if (file == NULL)
+ sysbail("cannot open %s", path);
+ if (fgets(buffer, sizeof(buffer), file) == NULL)
+ sysbail("cannot read from %s", path);
+ fclose(file);
+ pid = strtol(buffer, NULL, 10);
+ if (pid <= 0)
+ bail("cannot read PID from %s", path);
+ return (pid_t) pid;
+}
+
+
+/*
+ * Start a process and return its status information. The status information
+ * is also stored in the global processes linked list so that it can be
+ * stopped automatically on program exit.
+ *
+ * The boolean argument says whether to start the process under fakeroot. If
+ * true, PATH_FAKEROOT must be defined, generally by Autoconf. If it's not
+ * found, call skip_all.
+ *
+ * This is a helper function for process_start and process_start_fakeroot.
+ */
+static struct process *
+process_start_internal(const char *const argv[], const char *pidfile,
+ bool fakeroot)
+{
+ size_t i;
+ int log_fd;
+ const char *name;
+ struct timeval tv;
+ struct process *process;
+ const char **fakeroot_argv = NULL;
+ const char *path_fakeroot = PATH_FAKEROOT;
+
+ /* Check prerequisites. */
+ if (fakeroot && path_fakeroot[0] == '\0')
+ skip_all("fakeroot not found");
+
+ /* Create the process struct and log file. */
+ process = bcalloc(1, sizeof(struct process));
+ process->pidfile = bstrdup(pidfile);
+ process->tmpdir = test_tmpdir();
+ name = strrchr(argv[0], '/');
+ if (name != NULL)
+ name++;
+ else
+ name = argv[0];
+ basprintf(&process->logfile, "%s/%s.log.XXXXXX", process->tmpdir, name);
+ log_fd = mkstemp(process->logfile);
+ if (log_fd < 0)
+ sysbail("cannot create log file for %s", argv[0]);
+
+ /* If using fakeroot, rewrite argv accordingly. */
+ if (fakeroot) {
+ for (i = 0; argv[i] != NULL; i++)
+ ;
+ fakeroot_argv = bcalloc(2 + i + 1, sizeof(const char *));
+ fakeroot_argv[0] = path_fakeroot;
+ fakeroot_argv[1] = "--";
+ for (i = 0; argv[i] != NULL; i++)
+ fakeroot_argv[i + 2] = argv[i];
+ fakeroot_argv[i + 2] = NULL;
+ argv = fakeroot_argv;
+ }
+
+ /*
+ * Fork off the child process, redirect its standard output and standard
+ * error to the log file, and then exec the program.
+ */
+ process->pid = fork();
+ if (process->pid < 0)
+ sysbail("fork failed");
+ else if (process->pid == 0) {
+ if (dup2(log_fd, STDOUT_FILENO) < 0)
+ sysbail("cannot redirect standard output");
+ if (dup2(log_fd, STDERR_FILENO) < 0)
+ sysbail("cannot redirect standard error");
+ close(log_fd);
+ if (execv(argv[0], (char *const *) argv) < 0)
+ sysbail("exec of %s failed", argv[0]);
+ }
+ close(log_fd);
+ free(fakeroot_argv);
+
+ /*
+ * In the parent. Wait for the child to start by watching for the PID
+ * file to appear in 100ms intervals.
+ */
+ for (i = 0; i < PROCESS_WAIT * 10 && access(pidfile, F_OK) != 0; i++) {
+ tv.tv_sec = 0;
+ tv.tv_usec = 100000;
+ select(0, NULL, NULL, NULL, &tv);
+ }
+
+ /*
+ * If the PID file still hasn't appeared after ten seconds, attempt to
+ * kill the process and then bail.
+ */
+ if (access(pidfile, F_OK) != 0) {
+ kill(process->pid, SIGTERM);
+ alarm(5);
+ waitpid(process->pid, NULL, 0);
+ alarm(0);
+ bail("cannot start %s", argv[0]);
+ }
+
+ /*
+ * Read the PID back from the PID file. This usually isn't necessary for
+ * non-forking daemons, but always doing this makes this function general,
+ * and it's required when running under fakeroot.
+ */
+ if (fakeroot)
+ process->pid = read_pidfile(pidfile);
+ process->is_child = !fakeroot;
+
+ /* Register the log file as a source of diag messages. */
+ diag_file_add(process->logfile);
+
+ /*
+ * Add the process to our global list and set our cleanup handler if this
+ * is the first process we started.
+ */
+ if (processes == NULL)
+ test_cleanup_register(process_stop_all);
+ process->next = processes;
+ processes = process;
+
+ /* All done. */
+ return process;
+}
+
+
+/*
+ * Start a process and return the opaque process struct. The process must
+ * create pidfile with its PID when startup is complete.
+ */
+struct process *
+process_start(const char *const argv[], const char *pidfile)
+{
+ return process_start_internal(argv, pidfile, false);
+}
+
+
+/*
+ * Start a process under fakeroot and return the opaque process struct. If
+ * fakeroot is not available, calls skip_all. The process must create pidfile
+ * with its PID when startup is complete.
+ */
+struct process *
+process_start_fakeroot(const char *const argv[], const char *pidfile)
+{
+ return process_start_internal(argv, pidfile, true);
+}
diff --git a/tests/tap/process.h b/tests/tap/process.h
new file mode 100644
index 000000000000..4210c209ed0b
--- /dev/null
+++ b/tests/tap/process.h
@@ -0,0 +1,95 @@
+/*
+ * Utility functions for tests that use subprocesses.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Written by Russ Allbery <eagle@eyrie.org>
+ * Copyright 2009-2010, 2013
+ * The Board of Trustees of the Leland Stanford Junior University
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_PROCESS_H
+#define TAP_PROCESS_H 1
+
+#include <config.h>
+#include <tests/tap/macros.h>
+
+/* Opaque data type for process_start and friends. */
+struct process;
+
+BEGIN_DECLS
+
+/*
+ * Run a function in a subprocess and check the exit status and expected
+ * output (stdout and stderr combined) against the provided values. Expects
+ * the function to always exit (not die from a signal). data is optional data
+ * that's passed into the function as its only argument.
+ *
+ * This reports as three separate tests: whether the function exited rather
+ * than was killed, whether the exit status was correct, and whether the
+ * output was correct.
+ */
+typedef void (*test_function_type)(void *);
+void is_function_output(test_function_type, void *data, int status,
+ const char *output, const char *format, ...)
+ __attribute__((__format__(printf, 5, 6), __nonnull__(1)));
+
+/*
+ * Run a setup program. Takes the program to run and its arguments as an argv
+ * vector, where argv[0] must be either the full path to the program or the
+ * program name if the PATH should be searched. If the program does not exit
+ * successfully, call bail, with the error message being the output from the
+ * program.
+ */
+void run_setup(const char *const argv[]) __attribute__((__nonnull__));
+
+/*
+ * process_start starts a process in the background, returning an opaque data
+ * struct that can be used to stop the process later. The standard output and
+ * standard error of the process will be sent to a log file registered with
+ * diag_file_add, so its output will be properly interleaved with the test
+ * case output.
+ *
+ * The process should create a PID file in the path given as the second
+ * argument when it's finished initialization.
+ *
+ * process_start_fakeroot is the same but starts the process under fakeroot.
+ * PATH_FAKEROOT must be defined (generally by Autoconf). If fakeroot is not
+ * found, process_start_fakeroot will call skip_all, so be sure to call this
+ * function before plan.
+ *
+ * process_stop can be called to explicitly stop the process. If it isn't
+ * called by the test program, it will be called automatically when the
+ * program exits.
+ */
+struct process *process_start(const char *const argv[], const char *pidfile)
+ __attribute__((__nonnull__));
+struct process *process_start_fakeroot(const char *const argv[],
+ const char *pidfile)
+ __attribute__((__nonnull__));
+void process_stop(struct process *);
+
+END_DECLS
+
+#endif /* TAP_PROCESS_H */
diff --git a/tests/tap/string.c b/tests/tap/string.c
new file mode 100644
index 000000000000..71cf571e6f03
--- /dev/null
+++ b/tests/tap/string.c
@@ -0,0 +1,67 @@
+/*
+ * String utilities for the TAP protocol.
+ *
+ * Additional string utilities that can't be included with C TAP Harness
+ * because they rely on additional portability code from rra-c-util.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Copyright 2011-2012 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <config.h>
+#include <portable/system.h>
+
+#include <tests/tap/basic.h>
+#include <tests/tap/string.h>
+
+
+/*
+ * vsprintf into a newly allocated string, reporting a fatal error with bail
+ * on failure.
+ */
+void
+bvasprintf(char **strp, const char *fmt, va_list args)
+{
+ int status;
+
+ status = vasprintf(strp, fmt, args);
+ if (status < 0)
+ sysbail("failed to allocate memory for vasprintf");
+}
+
+
+/*
+ * sprintf into a newly allocated string, reporting a fatal error with bail on
+ * failure.
+ */
+void
+basprintf(char **strp, const char *fmt, ...)
+{
+ va_list args;
+
+ va_start(args, fmt);
+ bvasprintf(strp, fmt, args);
+ va_end(args);
+}
diff --git a/tests/tap/string.h b/tests/tap/string.h
new file mode 100644
index 000000000000..651c38a26f06
--- /dev/null
+++ b/tests/tap/string.h
@@ -0,0 +1,51 @@
+/*
+ * String utilities for the TAP protocol.
+ *
+ * Additional string utilities that can't be included with C TAP Harness
+ * because they rely on additional portability code from rra-c-util.
+ *
+ * The canonical version of this file is maintained in the rra-c-util package,
+ * which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+ *
+ * Copyright 2011-2012 Russ Allbery <eagle@eyrie.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef TAP_STRING_H
+#define TAP_STRING_H 1
+
+#include <config.h>
+#include <tests/tap/macros.h>
+
+#include <stdarg.h> /* va_list */
+
+BEGIN_DECLS
+
+/* sprintf into an allocated string, calling bail on any failure. */
+void basprintf(char **, const char *, ...)
+ __attribute__((__nonnull__, __format__(printf, 2, 3)));
+void bvasprintf(char **, const char *, va_list)
+ __attribute__((__nonnull__, __format__(printf, 2, 0)));
+
+END_DECLS
+
+#endif /* !TAP_STRING_H */
diff --git a/tests/valgrind/logs-t b/tests/valgrind/logs-t
new file mode 100755
index 000000000000..5444d00a5730
--- /dev/null
+++ b/tests/valgrind/logs-t
@@ -0,0 +1,83 @@
+#!/usr/bin/perl
+#
+# Check for errors in valgrind logs.
+#
+# The canonical version of this file is maintained in the rra-c-util package,
+# which can be found at <https://www.eyrie.org/~eagle/software/rra-c-util/>.
+#
+# Copyright 2018-2019, 2021 Russ Allbery <eagle@eyrie.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+#
+# SPDX-License-Identifier: MIT
+
+use 5.010;
+use strict;
+use warnings;
+
+use lib "$ENV{C_TAP_SOURCE}/tap/perl";
+
+use Test::RRA;
+use Test::RRA::Automake qw(automake_setup);
+
+use File::Spec;
+use Test::More;
+
+# Skip this test if C_TAP_VALGRIND was not set.
+if (!exists $ENV{C_TAP_VALGRIND}) {
+ plan skip_all => 'Not testing under valgrind';
+}
+
+# Set up Automake testing.
+automake_setup({ chdir_build => 1 });
+
+# Gather the list of valgrind logs (and skip this test if there are none).
+opendir(my $logdir, File::Spec->catfile('tests', 'tmp', 'valgrind'))
+ or plan skip_all => 'No valgrind logs in tests/tmp/valgrind';
+my @logs = grep { m{ \A log [.] }xms } readdir $logdir;
+closedir($logdir) or BAIL_OUT("cannot close directory: $!");
+
+# Check each log file.
+plan tests => scalar(@logs);
+for my $file (@logs) {
+ my $path = File::Spec->catfile('tests', 'tmp', 'valgrind', $file);
+ open(my $log, '<', $path) or BAIL_OUT("cannot open $path: $!");
+ my $okay = 1;
+ my @log;
+ while (defined(my $line = <$log>)) {
+ push(@log, $line);
+ if ($line =~ m{ ERROR [ ] SUMMARY: [ ] (\d+) [ ] errors }xms) {
+ $okay = ($1 == 0);
+ }
+ }
+ close($log) or BAIL_OUT("cannot close $path: $!");
+ if ($okay) {
+ unlink($path);
+ } else {
+ for my $line (@log) {
+ print '# ', $line
+ or BAIL_OUT("cannot print to standard output: $!");
+ }
+ }
+ ok($okay, $path);
+}
+
+# Remove tests/tmp/valgrind if it's now empty.
+rmdir(File::Spec->catfile('tests', 'tmp', 'valgrind'));
+rmdir(File::Spec->catfile('tests', 'tmp'));