ATF_TC_BODY(fork_wait__core_size, tc) { struct rlimit rl; rl.rlim_cur = 0; rl.rlim_max = RLIM_INFINITY; if (setrlimit(RLIMIT_CORE, &rl) == -1) atf_tc_skip("Failed to lower the core size limit"); kyua_run_params_t run_params; kyua_run_params_init(&run_params); pid_t pid; kyua_error_t error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) abort(); 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_TC_BODY(fork_wait__timeout, tc) { kyua_run_params_t run_params; kyua_run_params_init(&run_params); run_params.timeout_seconds = 1; pid_t pid; kyua_error_t error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { sigset_t mask; sigemptyset(&mask); for (;;) sigsuspend(&mask); } ATF_REQUIRE(!kyua_error_is_set(error)); int status; bool timed_out; kyua_run_wait(pid, &status, &timed_out); ATF_REQUIRE(timed_out); ATF_REQUIRE(WIFSIGNALED(status)); ATF_REQUIRE_EQ(SIGKILL, WTERMSIG(status)); }
/// 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); pid_t pid; kyua_error_t error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) kyua_run_exec(program, args); 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_REQUIRE(!timed_out); ATF_REQUIRE_MSG(WIFEXITED(status), "Subprocess expected to exit successfully"); *exitstatus = WEXITSTATUS(status); } return error; }
/// Forks and executes the cleanup of a test case in a controlled manner. /// /// \param test_program Path to the test program to execute. /// \param test_case Name of the test case to run. /// \param result_file Path to the ATF result file created by the body of the /// test case. The cleanup may update such file if it fails. /// \param user_variables Set of configuration variables to pass to the test. /// \param run_params Settings to control the subprocess. /// \param body_success The success value returned by run_body(). /// \param [out] success Set to true if the test case runs properly and returns /// a result that is to be considered as successful. /// /// \return OK if all goes well, an error otherwise. Note that a failed test /// case cleanup is denoted by setting success to false on exit, not by /// returning an error. static kyua_error_t run_cleanup(const char* test_program, const char* test_case, const char* result_file, const char* const user_variables[], const kyua_run_params_t* run_params, const bool body_success, bool* success) { kyua_error_t error; pid_t pid; error = kyua_run_fork(run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { exec_cleanup(test_program, test_case, user_variables); } assert(pid != -1 && pid != 0); if (kyua_error_is_set(error)) goto out; int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (kyua_error_is_set(error)) goto out; if (WIFSIGNALED(status) && WCOREDUMP(status)) { kyua_stacktrace_dump(test_program, pid, run_params, stderr); } if (body_success) { // If the body has reported a successful result, we inspect the status // of the cleanup routine. If the cleanup has failed, then we need to // mark the test as broken. However, if the body itself had failed, we // don't do this to give preference to the original result, which is // probably more informative. error = kyua_atf_result_cleanup_rewrite(result_file, status, timed_out, success); } out: return error; }
/// Forks and executes the body of a test case in a controlled manner. /// /// \param test_program Path to the test program to execute. /// \param test_case Name of the test case to run. /// \param result_file Path to the ATF result file to be created. /// \param user_variables Set of configuration variables to pass to the test. /// \param run_params Settings to control the subprocess. /// \param [out] success Set to true if the test case runs properly and returns /// a result that is to be considered as successful. /// /// \return OK if all goes well, an error otherwise. Note that a failed test /// case is denoted by setting success to false on exit, not by returning an /// error. static kyua_error_t run_body(const char* test_program, const char* test_case, const char* result_file, const char* const user_variables[], const kyua_run_params_t* run_params, bool* success) { kyua_error_t error; char* tmp_result_file; error = kyua_fs_concat(&tmp_result_file, run_params->work_directory, "result.txt", NULL); if (kyua_error_is_set(error)) goto out; pid_t pid; error = kyua_run_fork(run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { exec_body(test_program, test_case, tmp_result_file, user_variables); } assert(pid != -1 && pid != 0); if (kyua_error_is_set(error)) goto out_tmp_result_file; int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (kyua_error_is_set(error)) goto out_tmp_result_file; if (WIFSIGNALED(status) && WCOREDUMP(status)) { kyua_stacktrace_dump(test_program, pid, run_params, stderr); } error = kyua_atf_result_rewrite(tmp_result_file, result_file, status, timed_out, success); out_tmp_result_file: free(tmp_result_file); out: return error; }
/// 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)); char* work_directory; RE(kyua_run_work_directory_enter("template.XXXXXX", getuid(), getgid(), &work_directory)); kyua_run_params_t run_params; kyua_run_params_init(&run_params); run_params.work_directory = work_directory; pid_t pid; RE(kyua_run_fork(&run_params, &pid)); if (pid == 0) { sleep(run_params.timeout_seconds * 2); abort(); } // 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); int status; bool timed_out; RE(kyua_run_wait(pid, &status, &timed_out)); ATF_REQUIRE(!timed_out); ATF_REQUIRE(WIFSIGNALED(status)); ATF_REQUIRE_EQ(SIGKILL, WTERMSIG(status)); ATF_REQUIRE(rmdir(tmpdir) == -1); // Not yet empty. RE(kyua_run_work_directory_leave(&work_directory)); ATF_REQUIRE(rmdir(tmpdir) != -1); free(tmpdir); }
/// 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; }
/// Lists the test cases in a test program. /// /// \param test_program Path to the test program for which to list the test /// cases. Should be absolute. /// \param run_params Execution parameters to configure the test process. /// /// \return An error if the listing fails; OK otherwise. static kyua_error_t list_test_cases(const char* test_program, const kyua_run_params_t* run_params) { kyua_error_t error; char* work_directory; error = kyua_run_work_directory_enter(WORKDIR_TEMPLATE, run_params->unprivileged_user, run_params->unprivileged_group, &work_directory); if (kyua_error_is_set(error)) goto out; kyua_run_params_t real_run_params = *run_params; real_run_params.work_directory = work_directory; int stdout_fds[2]; if (pipe(stdout_fds) == -1) { error = kyua_libc_error_new(errno, "pipe failed"); goto out_work_directory; } pid_t pid; error = kyua_run_fork(&real_run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { run_list(test_program, stdout_fds); } assert(pid != -1 && pid != 0); if (kyua_error_is_set(error)) goto out_stdout_fds; FILE* tmp_output = NULL; // Initialize to shut up gcc warning. error = create_file_in_work_directory(real_run_params.work_directory, "list.txt", "w+", &tmp_output); if (kyua_error_is_set(error)) goto out_stdout_fds; close(stdout_fds[1]); stdout_fds[1] = -1; kyua_error_t parse_error = atf_list_parse(stdout_fds[0], tmp_output); stdout_fds[0] = -1; // Guaranteed closed by atf_list_parse. // Delay reporting of parse errors to later. If we detect a problem while // waiting for the test program, we know that the parsing has most likely // failed and therefore the error with the program is more important for // reporting purposes. int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (kyua_error_is_set(error)) goto out_tmp_output; if (!WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS) { error = kyua_generic_error_new("Test program list did not return " "success"); goto out_tmp_output; } error = kyua_error_subsume(error, parse_error); if (!kyua_error_is_set(error)) { rewind(tmp_output); error = dump_file(tmp_output, stdout); } out_tmp_output: fclose(tmp_output); out_stdout_fds: if (stdout_fds[0] != -1) close(stdout_fds[0]); if (stdout_fds[1] != -1) close(stdout_fds[1]); out_work_directory: error = kyua_error_subsume(error, kyua_run_work_directory_leave(&work_directory)); out: return error; }
/// Gathers a stacktrace of a crashed program. /// /// \param program The name of the binary that crashed and dumped a core file. /// Can be either absolute or relative. /// \param dead_pid The PID of the process that dumped core. /// \param original_run_params Parameters with which the original binary was /// executed. These are reused to run GDB, but adjusted with GDB-specific /// settings. Of special interest, the work directory is used to search for /// the core file. /// \param output Stream into which to dump the stack trace and any additional /// information. /// /// \post If anything goes wrong, the diagnostic messages are written to the /// output. This function returns no errors. void kyua_stacktrace_dump(const char* program, const pid_t dead_pid, const kyua_run_params_t* original_run_params, FILE* output) { fprintf(output, "Process with PID %d dumped core; attempting to gather " "stack trace\n", dead_pid); const kyua_run_params_t run_params = gdb_run_params(original_run_params); kyua_error_t error = kyua_error_ok(); char* core_file = kyua_stacktrace_find_core(const_basename(program), run_params.work_directory, dead_pid); if (core_file == NULL) { fprintf(output, "Cannot find any core file\n"); goto out; } // We must flush the output stream right before invoking fork, so that the // subprocess does not have any unflushed data. Failure to do so results in // the messages above being written twice to the output. fflush(output); pid_t pid; error = kyua_run_fork(&run_params, &pid); if (!kyua_error_is_set(error) && pid == 0) { run_gdb(program, core_file, output); } assert(pid != -1 && pid != 0); if (kyua_error_is_set(error)) goto out_core_file; int status; bool timed_out; error = kyua_run_wait(pid, &status, &timed_out); if (kyua_error_is_set(error)) goto out_core_file; if (timed_out) { fprintf(output, "GDB failed; timed out\n"); } else { if (WIFEXITED(status)) { if (WEXITSTATUS(status) == EXIT_SUCCESS) fprintf(output, "GDB exited successfully\n"); else fprintf(output, "GDB failed with code %d; see output above for " "details\n", WEXITSTATUS(status)); } else { assert(WIFSIGNALED(status)); fprintf(output, "GDB received signal %d; see output above for " "details\n", WTERMSIG(status)); } } out_core_file: free(core_file); out: if (kyua_error_is_set(error)) { kyua_error_fprintf(output, error, "Failed to gather stacktrace"); free(error); } }