diff options
Diffstat (limited to 'lib/util/tests/tfork.c')
-rw-r--r-- | lib/util/tests/tfork.c | 855 |
1 files changed, 855 insertions, 0 deletions
diff --git a/lib/util/tests/tfork.c b/lib/util/tests/tfork.c new file mode 100644 index 0000000..70ae975 --- /dev/null +++ b/lib/util/tests/tfork.c @@ -0,0 +1,855 @@ +/* + * Tests for tfork + * + * Copyright Ralph Boehme <slow@samba.org> 2017 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "replace.h" +#include <talloc.h> +#include <tevent.h> +#include "system/filesys.h" +#include "system/wait.h" +#include "system/select.h" +#include "libcli/util/ntstatus.h" +#include "torture/torture.h" +#include "lib/util/data_blob.h" +#include "torture/local/proto.h" +#include "lib/util/tfork.h" +#include "lib/util/samba_util.h" +#include "lib/util/sys_rw.h" +#ifdef HAVE_PTHREAD +#include <pthread.h> +#endif + +static bool test_tfork_simple(struct torture_context *tctx) +{ + pid_t parent = getpid(); + struct tfork *t = NULL; + pid_t child; + int ret; + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + torture_comment(tctx, "my parent pid is %d\n", parent); + torture_assert(tctx, getpid() != parent, "tfork failed\n"); + _exit(0); + } + + ret = tfork_destroy(&t); + torture_assert(tctx, ret == 0, "tfork_destroy failed\n"); + + return true; +} + +static bool test_tfork_status(struct torture_context *tctx) +{ + struct tfork *t = NULL; + int status; + pid_t child; + bool ok = true; + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + _exit(123); + } + + status = tfork_status(&t, true); + if (status == -1) { + torture_fail(tctx, "tfork_status failed\n"); + } + + torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done, + "tfork failed\n"); + torture_assert_goto(tctx, WEXITSTATUS(status) == 123, ok, done, + "tfork failed\n"); + + torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status)); + +done: + return ok; +} + +static bool test_tfork_sigign(struct torture_context *tctx) +{ + struct tfork *t = NULL; + struct sigaction act; + pid_t child; + int status; + bool ok = true; + int ret; + + act = (struct sigaction) { + .sa_flags = SA_NOCLDWAIT, + .sa_handler = SIG_IGN, + }; + + ret = sigaction(SIGCHLD, &act, NULL); + torture_assert_goto(tctx, ret == 0, ok, done, "sigaction failed\n"); + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + sleep(1); + _exit(123); + } + + child = fork(); + if (child == -1) { + torture_fail(tctx, "fork failed\n"); + return false; + } + if (child == 0) { + _exit(0); + } + + status = tfork_status(&t, true); + if (status == -1) { + torture_fail(tctx, "tfork_status failed\n"); + } + + torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done, + "tfork failed\n"); + torture_assert_goto(tctx, WEXITSTATUS(status) == 123, ok, done, + "tfork failed\n"); + torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status)); + +done: + return ok; +} + +static void sigchld_handler1(int signum, siginfo_t *si, void *u) +{ + pid_t pid; + int status; + + if (signum != SIGCHLD) { + abort(); + } + + pid = waitpid(si->si_pid, &status, 0); + if (pid != si->si_pid) { + abort(); + } +} + +static bool test_tfork_sighandler(struct torture_context *tctx) +{ + struct tfork *t = NULL; + struct sigaction act; + struct sigaction oldact; + pid_t child; + int status; + bool ok = true; + int ret; + + act = (struct sigaction) { + .sa_flags = SA_SIGINFO, + .sa_sigaction = sigchld_handler1, + }; + + ret = sigaction(SIGCHLD, &act, &oldact); + torture_assert_goto(tctx, ret == 0, ok, done, "sigaction failed\n"); + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + sleep(1); + _exit(123); + } + + child = fork(); + if (child == -1) { + torture_fail(tctx, "fork failed\n"); + return false; + } + if (child == 0) { + _exit(0); + } + + status = tfork_status(&t, true); + if (status == -1) { + torture_fail(tctx, "tfork_status failed\n"); + } + + torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done, + "tfork failed\n"); + torture_assert_goto(tctx, WEXITSTATUS(status) == 123, ok, done, + "tfork failed\n"); + torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status)); + +done: + sigaction(SIGCHLD, &oldact, NULL); + + return ok; +} + +static bool test_tfork_process_hierarchy(struct torture_context *tctx) +{ + struct tfork *t = NULL; + pid_t pid = getpid(); + pid_t child; + pid_t pgid = getpgid(0); + pid_t sid = getsid(0); + char *procpath = NULL; + int status; + struct stat st; + int ret; + bool ok = true; + + procpath = talloc_asprintf(tctx, "/proc/%d/status", getpid()); + torture_assert_not_null(tctx, procpath, "talloc_asprintf failed\n"); + + ret = stat(procpath, &st); + TALLOC_FREE(procpath); + if (ret != 0) { + if (errno == ENOENT) { + torture_skip(tctx, "/proc missing\n"); + } + torture_fail(tctx, "stat failed\n"); + } + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + char *cmd = NULL; + FILE *fp = NULL; + char line[64]; + char *p; + pid_t ppid; + + torture_assert_goto(tctx, pgid == getpgid(0), ok, child_fail, "tfork failed\n"); + torture_assert_goto(tctx, sid == getsid(0), ok, child_fail, "tfork failed\n"); + + cmd = talloc_asprintf(tctx, "cat /proc/%d/status | awk '/^PPid:/ {print $2}'", getppid()); + torture_assert_goto(tctx, cmd != NULL, ok, child_fail, "talloc_asprintf failed\n"); + + fp = popen(cmd, "r"); + torture_assert_goto(tctx, fp != NULL, ok, child_fail, "popen failed\n"); + + p = fgets(line, sizeof(line) - 1, fp); + pclose(fp); + torture_assert_goto(tctx, p != NULL, ok, child_fail, "popen failed\n"); + + ret = sscanf(line, "%d", &ppid); + torture_assert_goto(tctx, ret == 1, ok, child_fail, "sscanf failed\n"); + torture_assert_goto(tctx, ppid == pid, ok, child_fail, "process hierarchy not rooted at caller\n"); + + _exit(0); + + child_fail: + _exit(1); + } + + status = tfork_status(&t, true); + if (status == -1) { + torture_fail(tctx, "tfork_status failed\n"); + } + + torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done, + "tfork failed\n"); + torture_assert_goto(tctx, WEXITSTATUS(status) == 0, ok, done, + "tfork failed\n"); + torture_comment(tctx, "exit status [%d]\n", WEXITSTATUS(status)); + +done: + return ok; +} + +static bool test_tfork_pipe(struct torture_context *tctx) +{ + struct tfork *t = NULL; + int status; + pid_t child; + int up[2]; + int down[2]; + char c; + int ret; + bool ok = true; + + ret = pipe(&up[0]); + torture_assert(tctx, ret == 0, "pipe failed\n"); + + ret = pipe(&down[0]); + torture_assert(tctx, ret == 0, "pipe failed\n"); + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + close(up[0]); + close(down[1]); + + ret = read(down[0], &c, 1); + torture_assert_goto(tctx, ret == 1, ok, child_fail, "read failed\n"); + torture_assert_goto(tctx, c == 1, ok, child_fail, "read failed\n"); + + ret = write(up[1], &(char){2}, 1); + torture_assert_goto(tctx, ret == 1, ok, child_fail, "write failed\n"); + + _exit(0); + + child_fail: + _exit(1); + } + + close(up[1]); + close(down[0]); + + ret = write(down[1], &(char){1}, 1); + torture_assert(tctx, ret == 1, "read failed\n"); + + ret = read(up[0], &c, 1); + torture_assert(tctx, ret == 1, "read failed\n"); + torture_assert(tctx, c == 2, "read failed\n"); + + status = tfork_status(&t, true); + if (status == -1) { + torture_fail(tctx, "tfork_status failed\n"); + } + + torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done, + "tfork failed\n"); + torture_assert_goto(tctx, WEXITSTATUS(status) == 0, ok, done, + "tfork failed\n"); +done: + return ok; +} + +static bool test_tfork_twice(struct torture_context *tctx) +{ + struct tfork *t = NULL; + int status; + pid_t child; + pid_t pid; + int up[2]; + int ret; + bool ok = true; + + ret = pipe(&up[0]); + torture_assert(tctx, ret == 0, "pipe failed\n"); + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + close(up[0]); + + t = tfork_create(); + if (t == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child = tfork_child_pid(t); + if (child == 0) { + sleep(1); + pid = getpid(); + ret = write(up[1], &pid, sizeof(pid_t)); + torture_assert_goto(tctx, ret == sizeof(pid_t), ok, child_fail, "write failed\n"); + + _exit(0); + + child_fail: + _exit(1); + } + + _exit(0); + } + + close(up[1]); + + ret = read(up[0], &pid, sizeof(pid_t)); + torture_assert(tctx, ret == sizeof(pid_t), "read failed\n"); + + status = tfork_status(&t, true); + torture_assert_goto(tctx, status != -1, ok, done, "tfork_status failed\n"); + + torture_assert_goto(tctx, WIFEXITED(status) == true, ok, done, + "tfork failed\n"); + torture_assert_goto(tctx, WEXITSTATUS(status) == 0, ok, done, + "tfork failed\n"); +done: + return ok; +} + +static void *tfork_thread(void *p) +{ + struct tfork *t = NULL; + int status; + pid_t child; + uint64_t tid = (uint64_t)pthread_self(); + uint64_t *result = NULL; + int up[2]; + ssize_t nread; + int ret; + + ret = pipe(up); + if (ret != 0) { + pthread_exit(NULL); + } + + t = tfork_create(); + if (t == NULL) { + pthread_exit(NULL); + } + child = tfork_child_pid(t); + if (child == 0) { + ssize_t nwritten; + + close(up[0]); + tid++; + nwritten = sys_write(up[1], &tid, sizeof(uint64_t)); + if (nwritten != sizeof(uint64_t)) { + _exit(1); + } + _exit(0); + } + close(up[1]); + + result = malloc(sizeof(uint64_t)); + if (result == NULL) { + pthread_exit(NULL); + } + + nread = sys_read(up[0], result, sizeof(uint64_t)); + if (nread != sizeof(uint64_t)) { + pthread_exit(NULL); + } + + status = tfork_status(&t, true); + if (status == -1) { + pthread_exit(NULL); + } + + pthread_exit(result); +} + +static bool test_tfork_threads(struct torture_context *tctx) +{ + int ret; + bool ok = true; + const int num_threads = 64; + pthread_t threads[num_threads]; + sigset_t set; + int i; + +#ifndef HAVE_PTHREAD + torture_skip(tctx, "no pthread support\n"); +#endif + + /* + * Be nasty and taste for the worst case: ensure all threads start with + * SIGCHLD unblocked so we have the most fun with SIGCHLD being + * delivered to a random thread. :) + */ + sigemptyset(&set); + sigaddset(&set, SIGCHLD); +#ifdef HAVE_PTHREAD + ret = pthread_sigmask(SIG_UNBLOCK, &set, NULL); +#else + ret = sigprocmask(SIG_UNBLOCK, &set, NULL); +#endif + if (ret != 0) { + return false; + } + + for (i = 0; i < num_threads; i++) { + ret = pthread_create(&threads[i], NULL, tfork_thread, NULL); + torture_assert_goto(tctx, ret == 0, ok, done, + "pthread_create failed\n"); + } + + for (i = 0; i < num_threads; i++) { + void *p; + uint64_t *result; + + ret = pthread_join(threads[i], &p); + torture_assert_goto(tctx, ret == 0, ok, done, + "pthread_join failed\n"); + result = (uint64_t *)p; + torture_assert_goto(tctx, *result == (uint64_t)threads[i] + 1, + ok, done, "thread failed\n"); + free(p); + } + +done: + return ok; +} + +static bool test_tfork_cmd_send(struct torture_context *tctx) +{ + struct tevent_context *ev = NULL; + struct tevent_req *req = NULL; + const char *cmd[2] = { NULL, NULL }; + bool ok = true; + + ev = tevent_context_init(tctx); + torture_assert_goto(tctx, ev != NULL, ok, done, + "tevent_context_init failed\n"); + + cmd[0] = talloc_asprintf(tctx, "%s/testprogs/blackbox/tfork.sh", SRCDIR); + torture_assert_goto(tctx, cmd[0] != NULL, ok, done, + "talloc_asprintf failed\n"); + + req = samba_runcmd_send(tctx, ev, timeval_zero(), 0, 0, + cmd, "foo", NULL); + torture_assert_goto(tctx, req != NULL, ok, done, + "samba_runcmd_send failed\n"); + + ok = tevent_req_poll(req, ev); + torture_assert_goto(tctx, ok, ok, done, "tevent_req_poll failed\n"); + + torture_comment(tctx, "samba_runcmd_send test finished\n"); + +done: + TALLOC_FREE(ev); + + return ok; +} + +/* + * Test to ensure that the event_fd becomes readable after + * a tfork_process terminates. + */ +static bool test_tfork_event_file_handle(struct torture_context *tctx) +{ + bool ok = true; + + struct tfork *t1 = NULL; + pid_t child1; + struct pollfd poll1[] = { + { + .fd = -1, + .events = POLLIN, + }, + }; + + struct tfork *t2 = NULL; + pid_t child2; + struct pollfd poll2[] = { + { + .fd = -1, + .events = POLLIN, + }, + }; + + + t1 = tfork_create(); + if (t1 == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + + child1 = tfork_child_pid(t1); + if (child1 == 0) { + /* + * Parent process will kill this with a SIGTERM + * so 10 seconds should be plenty + */ + sleep(10); + exit(1); + } + poll1[0].fd = tfork_event_fd(t1); + + t2 = tfork_create(); + if (t2 == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child2 = tfork_child_pid(t2); + if (child2 == 0) { + /* + * Parent process will kill this with a SIGTERM + * so 10 seconds should be plenty + */ + sleep(10); + exit(2); + } + poll2[0].fd = tfork_event_fd(t2); + + /* + * Have forked two process and are in the master process + * Expect that both event_fds are unreadable + */ + poll(poll1, 1, 0); + ok = !(poll1[0].revents & POLLIN); + torture_assert_goto(tctx, ok, ok, done, + "tfork process 1 event fd readable\n"); + poll(poll2, 1, 0); + ok = !(poll2[0].revents & POLLIN); + torture_assert_goto(tctx, ok, ok, done, + "tfork process 1 event fd readable\n"); + + /* Kill the first child process */ + kill(child1, SIGKILL); + sleep(1); + + /* + * Have killed the first child, so expect it's event_fd to have gone + * readable. + * + */ + poll(poll1, 1, 0); + ok = (poll1[0].revents & POLLIN); + torture_assert_goto(tctx, ok, ok, done, + "tfork process 1 event fd not readable\n"); + poll(poll2, 1, 0); + ok = !(poll2[0].revents & POLLIN); + torture_assert_goto(tctx, ok, ok, done, + "tfork process 2 event fd readable\n"); + + /* Kill the secind child process */ + kill(child2, SIGKILL); + sleep(1); + /* + * Have killed the children, so expect their event_fd's to have gone + * readable. + * + */ + poll(poll1, 1, 0); + ok = (poll1[0].revents & POLLIN); + torture_assert_goto(tctx, ok, ok, done, + "tfork process 1 event fd not readable\n"); + poll(poll2, 1, 0); + ok = (poll2[0].revents & POLLIN); + torture_assert_goto(tctx, ok, ok, done, + "tfork process 2 event fd not readable\n"); + +done: + free(t1); + free(t2); + + return ok; +} + +/* + * Test to ensure that the status calls behave as expected after a process + * terminates. + * + * As the parent process owns the status fd's they get passed to all + * subsequent children after a tfork. So it's possible for another + * child process to hold the status pipe open. + * + * The event fd needs to be left open by tfork, as a close in the status + * code can cause issues in tevent code. + * + */ +static bool test_tfork_status_handle(struct torture_context *tctx) +{ + bool ok = true; + + struct tfork *t1 = NULL; + pid_t child1; + + struct tfork *t2 = NULL; + pid_t child2; + + int status; + int fd; + int ev1_fd; + int ev2_fd; + + + t1 = tfork_create(); + if (t1 == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + + child1 = tfork_child_pid(t1); + if (child1 == 0) { + /* + * Parent process will kill this with a SIGTERM + * so 10 seconds should be plenty + */ + sleep(10); + exit(1); + } + ev1_fd = tfork_event_fd(t1); + + t2 = tfork_create(); + if (t2 == NULL) { + torture_fail(tctx, "tfork failed\n"); + return false; + } + child2 = tfork_child_pid(t2); + if (child2 == 0) { + /* + * Parent process will kill this with a SIGTERM + * so 10 seconds should be plenty + */ + sleep(10); + exit(2); + } + ev2_fd = tfork_event_fd(t2); + + /* + * Have forked two process and are in the master process + * expect that the status call will block, and hence return -1 + * as the processes are still running + * The event fd's should be open. + */ + status = tfork_status(&t1, false); + ok = status == -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork status available for non terminated " + "process 1\n"); + /* Is the event fd open? */ + fd = dup(ev1_fd); + ok = fd != -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork process 1 event fd is not open"); + + status = tfork_status(&t2, false); + ok = status == -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork status available for non terminated " + "process 2\n"); + /* Is the event fd open? */ + fd = dup(ev2_fd); + ok = fd != -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork process 2 event fd is not open"); + + /* + * Kill the first process, it's status should be readable + * and it's event_fd should be open + * The second process's status should be unreadable. + */ + kill(child1, SIGTERM); + sleep(1); + status = tfork_status(&t1, false); + ok = status != -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork status for child 1 not available after " + "termination\n"); + /* Is the event fd open? */ + fd = dup(ev2_fd); + ok = fd != -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork process 1 event fd is not open"); + + status = tfork_status(&t2, false); + ok = status == -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork status available for child 2 after " + "termination of child 1\n"); + + /* + * Kill the second process, it's status should be readable + */ + kill(child2, SIGTERM); + sleep(1); + status = tfork_status(&t2, false); + ok = status != -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork status for child 2 not available after " + "termination\n"); + + /* Check that the event fd's are still open */ + /* Is the event fd open? */ + fd = dup(ev1_fd); + ok = fd != -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork process 1 event fd is not open"); + /* Is the event fd open? */ + fd = dup(ev2_fd); + ok = fd != -1; + torture_assert_goto(tctx, ok, ok, done, + "tfork process 2 event fd is not open"); + +done: + return ok; +} + +struct torture_suite *torture_local_tfork(TALLOC_CTX *mem_ctx) +{ + struct torture_suite *suite = + torture_suite_create(mem_ctx, "tfork"); + + torture_suite_add_simple_test(suite, + "tfork_simple", + test_tfork_simple); + + torture_suite_add_simple_test(suite, + "tfork_status", + test_tfork_status); + + torture_suite_add_simple_test(suite, + "tfork_sigign", + test_tfork_sigign); + + torture_suite_add_simple_test(suite, + "tfork_sighandler", + test_tfork_sighandler); + + torture_suite_add_simple_test(suite, + "tfork_process_hierarchy", + test_tfork_process_hierarchy); + + torture_suite_add_simple_test(suite, + "tfork_pipe", + test_tfork_pipe); + + torture_suite_add_simple_test(suite, + "tfork_twice", + test_tfork_twice); + + torture_suite_add_simple_test(suite, + "tfork_threads", + test_tfork_threads); + + torture_suite_add_simple_test(suite, + "tfork_cmd_send", + test_tfork_cmd_send); + + torture_suite_add_simple_test(suite, + "tfork_event_file_handle", + test_tfork_event_file_handle); + + torture_suite_add_simple_test(suite, + "tfork_status_handle", + test_tfork_status_handle); + + return suite; +} |