tests util/tfork: Tests for status and event fd
authorGary Lockyer <gary@catalyst.net.nz>
Mon, 11 Sep 2017 02:48:21 +0000 (14:48 +1200)
committerRalph Boehme <slow@samba.org>
Sat, 16 Sep 2017 17:53:22 +0000 (19:53 +0200)
Add tests to ensure that:
- The event_fd becomes readable once the worker process has terminated
- That the event_fd is not closed by the tfork code.
  - If this is done in tevent code and the event fde has not been
    freed, "Bad talloc magic value - " errors can result.
- That the status call does not block if the parent process launches
  more than one child process.
  - The status file descriptor for a child is passed to the
    subsequent children.  These processes hold the FD open, so that
    closing the fd does not make the read end go readable, and the
    process calling status blocks.

Bug: https://bugzilla.samba.org/show_bug.cgi?id=13037

Signed-off-by: Gary Lockyer <gary@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Ralph Boehme <slow@samba.org>
lib/util/tests/tfork.c
selftest/knownfail.d/tfork [new file with mode: 0644]

index c9005021b8a11d75dcddae82063e4cd61f954035..bf642fe37c96287f6541dea723d90139ab57409c 100644 (file)
@@ -22,6 +22,7 @@
 #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"
@@ -533,6 +534,248 @@ done:
        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[] = { {-1, POLLIN} };
+
+       struct tfork *t2 = NULL;
+       pid_t child2;
+       struct pollfd poll2[] = { {-1, 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:
+       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 avaiable 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 =
@@ -574,5 +817,13 @@ struct torture_suite *torture_local_tfork(TALLOC_CTX *mem_ctx)
                                      "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;
 }
diff --git a/selftest/knownfail.d/tfork b/selftest/knownfail.d/tfork
new file mode 100644 (file)
index 0000000..ea06b01
--- /dev/null
@@ -0,0 +1 @@
+samba4.local.tfork.tfork_status_handle\(none\)