// Copyright 2012 Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of Google Inc. nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#if defined(HAVE_CONFIG_H)
# include "config.h"
#endif
/// Evalutes an expression and ensures it does not return an error.
///
/// \param expr A expression that must evaluate to kyua_error_t.
#define RE(expr) ATF_REQUIRE(!kyua_error_is_set(expr))
/// Subprocess that validates the cleanliness of the environment.
///
/// \param unused_cookie NULL.
///
/// \post Exits with success if the environment is clean; failure otherwise.
static void
check_env(const void* KYUA_DEFS_UNUSED_PARAM(cookie))
{
bool failed = false;
if (strcmp(getenv("HOME"), ".") != 0) {
failed = true;
printf("HOME was not set to .\n");
}
if (strcmp(getenv("TZ"), "UTC") != 0) {
failed = true;
printf("TZ was not set to UTC\n");
}
if (strcmp(getenv("LEAVE_ME_ALONE"), "kill-some-day") != 0) {
failed = true;
printf("LEAVE_ME_ALONE was modified while it should not have been\n");
}
/// Subprocess that validates that it has become the leader of a process group.
///
/// \param unused_cookie NULL.
///
/// \post Exits with success if the process lives in its own process group;
/// failure otherwise.
static void
check_process_group(const void* KYUA_DEFS_UNUSED_PARAM(cookie))
{
exit(getpgid(getpid()) == getpid() ? EXIT_SUCCESS : EXIT_FAILURE);
}
/// Subprocess that validates that signals have been reset to their defaults.
///
/// \param unused_cookie NULL.
///
/// \post Exits with success if the process has its signals reset to their
/// default values; failure otherwise.
static void
check_signals(const void* KYUA_DEFS_UNUSED_PARAM(cookie))
{
int signo;
for (signo = 1; signo <= LAST_SIGNO; signo++) {
if (signo == SIGKILL || signo == SIGSTOP) {
// Don't attempt to check immutable signals, as this results in
// an unconditional error in some systems. E.g. Mac OS X 10.8
// reports 'Invalid argument' when querying SIGKILL.
continue;
}
struct sigaction old_sa;
if (sigaction(signo, NULL, &old_sa) == -1) {
err(EXIT_FAILURE, "Failed to query signal information for %d",
signo);
}
if (old_sa.sa_handler != SIG_DFL) {
errx(EXIT_FAILURE, "Signal %d not reset to its default handler",
signo);
}
printf("Signal %d has its default handler set\n", signo);
}
/// Subprocess that validates that the umask has been reset.
///
/// \param unused_cookie NULL.
///
/// \post Exits with success if the umask matches the expected value; failure
/// otherwise.
static void
check_umask(const void* KYUA_DEFS_UNUSED_PARAM(cookie))
{
exit(umask(0) == 0022 ? EXIT_SUCCESS : EXIT_FAILURE);
}
/// Subprocess that validates that the umask has been reset.
///
/// \param cookie The name of a file to expect in the current directory.
///
/// \post Exits with success if the umask matches the expected value; failure
/// otherwise.
static void
check_work_directory(const void* cookie)
{
const char* exp_file = (const char*)cookie;
exit(atf_utils_file_exists(exp_file) ? EXIT_SUCCESS : EXIT_FAILURE);
}
/// Subprocess that validates that the UID is not root.
///
/// \param unused_cookie NULL.
///
/// \post Exits with success if the UID is not root; failure otherwise.
static void
check_uid_not_root(const void* KYUA_DEFS_UNUSED_PARAM(cookie))
{
exit(getuid() != 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}
/// Subprocess that validates that the GID is not root.
///
/// \param unused_cookie NULL.
///
/// \post Exits with success if the GID is not root; failure otherwise.
static void
check_gid_not_root(const void* KYUA_DEFS_UNUSED_PARAM(cookie))
{
exit(getgid() != 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}
/// Subprocess that validates that the UID and GID are not root.
///
/// \param unused_cookie NULL.
///
/// \post Exits with success if the UID and GID are not root; failure otherwise.
static void
check_not_root(const void* KYUA_DEFS_UNUSED_PARAM(cookie))
{
exit(getuid() != 0 && getgid() != 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}
/// Uses kyua_fork, kyua_exec and kyua_wait to execute a subprocess.
///
/// \param program Path to the program to run.
/// \param args Arguments to the program.
/// \param [out] exitstatus The exit status of the subprocess, if it exits
/// successfully without timing out nor receiving a signal.
///
/// \return Returns the error code of kyua_run_wait (which should have the
/// error representation of the exec call in the subprocess).
static kyua_error_t
exec_check(const char* program, const char* const* args, int* exitstatus)
{
kyua_run_params_t run_params;
kyua_run_params_init(&run_params);
/// Uses kyua_fork and kyua_wait to spawn a subprocess.
///
/// \param run_params The parameters to configure the subprocess. Can be NULL
/// to indicate to use the default set of parameters.
/// \param hook Any of the check_* functions provided in this module.
/// \param cookie The data to pass to the hook.
///
/// \return True if the subprocess exits successfully; false otherwise.
static bool
fork_check(const kyua_run_params_t* run_params,
void (*hook)(const void*), const void* cookie)
{
kyua_run_params_t default_run_params;
if (run_params == NULL) {
kyua_run_params_init(&default_run_params);
run_params = &default_run_params;
}
pid_t pid;
kyua_error_t error = kyua_run_fork(run_params, &pid);
if (!kyua_error_is_set(error) && pid == 0)
hook(cookie);
ATF_REQUIRE(!kyua_error_is_set(error));
int status; bool timed_out;
error = kyua_run_wait(pid, &status, &timed_out);
if (kyua_error_is_set(error))
atf_tc_fail("wait failed; unexpected problem during exec?");
ATF_REQUIRE(!timed_out);
return WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS;
}
ATF_REQUIRE(!kyua_error_is_set(error));
int status; bool timed_out;
error = kyua_run_wait(pid, &status, &timed_out);
if (kyua_error_is_set(error))
atf_tc_fail("wait failed; unexpected problem during exec?");
ATF_REQUIRE(!timed_out);
ATF_REQUIRE(WIFSIGNALED(status));
ATF_REQUIRE_MSG(WCOREDUMP(status), "Core not dumped as expected");
}
ATF_REQUIRE_MSG(fork_check(NULL, check_env, NULL),
"Unclean environment in subprocess");
}
ATF_TC_WITHOUT_HEAD(fork_wait__process_group);
ATF_TC_BODY(fork_wait__process_group, tc)
{
ATF_REQUIRE_MSG(fork_check(NULL, check_process_group, NULL),
"Subprocess not in its own process group");
}
ATF_TC_WITHOUT_HEAD(fork_wait__signals);
ATF_TC_BODY(fork_wait__signals, tc)
{
ATF_REQUIRE_MSG(LAST_SIGNO > 10, "LAST_SIGNO as detected by configure is "
"suspiciously low");
int signo;
for (signo = 1; signo <= LAST_SIGNO; signo++) {
if (signo == SIGKILL || signo == SIGSTOP) {
// Ignore immutable signals.
continue;
}
if (signo == SIGCHLD) {
// If we were to reset SIGCHLD to SIG_IGN (which is different than
// not touching the signal at all, leaving it at its default value),
// our child process will not become a zombie and the call to
// kyua_run_wait will fail. Avoid this.
continue;
}
ATF_TC_WITHOUT_HEAD(fork_wait__umask);
ATF_TC_BODY(fork_wait__umask, tc)
{
(void)umask(0222);
ATF_REQUIRE_MSG(fork_check(NULL, check_umask, NULL),
"Subprocess does not have the predetermined 0022 umask");
}
kyua_run_params_t run_params;
kyua_run_params_init(&run_params);
run_params.work_directory = "./the-work-directory";
ATF_REQUIRE_MSG(fork_check(&run_params, check_work_directory, "data-file"),
"Subprocess not in its own process group");
}
ATF_REQUIRE(rmdir(tmpdir) != -1); // Empty; subdirectory not created.
free(tmpdir);
}
/// Performs a signal delivery test to the work directory handling code.
///
/// \param signo The signal to deliver.
static void
work_directory_signal_check(const int signo)
{
char* tmpdir;
RE(kyua_fs_make_absolute("worktest", &tmpdir));
ATF_REQUIRE(mkdir(tmpdir, 0755) != -1);
RE(kyua_env_set("TMPDIR", tmpdir));
// This should cause the handled installed by the work_directory management
// code to terminate the subprocess so that we get a chance to run the
// cleanup code ourselves.
kill(getpid(), signo);