[PATCH] Add ssdd test to the rt-tests suite

From: John Kacur
Date: Mon Jan 14 2019 - 12:09:29 EST


From: Joe Korty <joe.korty@xxxxxxxxxxxxxxxxx>

The following program might make a good addition to the rt
test suite. It tests the reliability of PTRACE_SINGLESTEP.
It does by default 10,000 ssteps against a simple,
spinner tracee. Also by default, it spins off ten of these
tracer/tracee pairs, all of which are to run concurrently.

Starting with 4.13-rt, this test occasionally encounters a
sstep whose waitpid returns a WIFSIGNALED (signal SIGTRAP)
rather than a WIFSTOPPED. This usually happens after
thousands of ssteps have executed. Having multiple
tracer/tracee pairs running dramatically increases the
chances of failure.

The is what the test output looks like for a good run:

forktest#0/22872: STARTING
forktest#7/22879: STARTING
forktest#8/22880: STARTING
forktest#6/22878: STARTING
forktest#5/22877: STARTING
forktest#3/22875: STARTING
forktest#4/22876: STARTING
forktest#9/22882: STARTING
forktest#2/22874: STARTING
forktest#1/22873: STARTING
forktest#0/22872: EXITING, no error
forktest#8/22880: EXITING, no error
forktest#3/22875: EXITING, no error
forktest#7/22879: EXITING, no error
forktest#6/22878: EXITING, no error
forktest#5/22877: EXITING, no error
forktest#2/22874: EXITING, no error
forktest#4/22876: EXITING, no error
forktest#9/22882: EXITING, no error
forktest#1/22873: EXITING, no error
All tests PASSED.

Signed-off-by: Joe Korty <joe.korty@xxxxxxxxxxxxxxxxx>
Signed-off-by: John Kacur <jkacur@xxxxxxxxxx>
---
src/ssdd/ssdd.c | 315 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 315 insertions(+)
create mode 100644 src/ssdd/ssdd.c

diff --git a/src/ssdd/ssdd.c b/src/ssdd/ssdd.c
new file mode 100644
index 000000000000..6d09d54e34e1
--- /dev/null
+++ b/src/ssdd/ssdd.c
@@ -0,0 +1,315 @@
+/*
+ * Have a tracer do a bunch of PTRACE_SINGLESTEPs against
+ * a tracee as fast as possible. Create several of these
+ * tracer/tracee pairs and see if they can be made to
+ * interfere with each other.
+ *
+ * Usage:
+ * ssdd nforks niters
+ * Where:
+ * nforks - number of tracer/tracee pairs to fork off.
+ * default 10.
+ * niters - number of PTRACE_SINGLESTEP iterations to
+ * do before declaring success, for each tracer/
+ * tracee pair set up. Default 10,000.
+ *
+ * The tracer waits on each PTRACE_SINGLESTEP with a waitpid(2)
+ * and checks that waitpid's return values for correctness.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <signal.h>
+#include <errno.h>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/ptrace.h>
+
+/* do_wait return values */
+#define STATE_EXITED 1
+#define STATE_STOPPED 2
+#define STATE_SIGNALED 3
+#define STATE_UNKNOWN 4
+#define STATE_ECHILD 5
+#define STATE_EXITED_TSIG 6 /* exited with termination signal */
+#define STATE_EXITED_ERRSTAT 7 /* exited with non-zero status */
+
+char *state_name[] = {
+ [STATE_EXITED] = "STATE_EXITED",
+ [STATE_STOPPED] = "STATE_STOPPED",
+ [STATE_SIGNALED] = "STATE_SIGNALED",
+ [STATE_UNKNOWN] = "STATE_UNKNOWN",
+ [STATE_ECHILD] = "STATE_ECHILD",
+ [STATE_EXITED_TSIG] = "STATE_EXITED_TSIG",
+ [STATE_EXITED_ERRSTAT] = "STATE_EXITED_ERRSTAT"
+};
+
+const char *get_state_name(int state)
+{
+ if (state < STATE_EXITED || state > STATE_EXITED_ERRSTAT)
+ return "?";
+ return state_name[state];
+}
+
+#define unused __attribute__((unused))
+
+static int got_sigchld;
+
+static int do_wait(pid_t *wait_pid, int *ret_sig)
+{
+ int status, child_status;
+
+ *ret_sig = -1; /* initially mark 'nothing returned' */
+
+ while (1) {
+ status = waitpid(-1, &child_status, WUNTRACED | __WALL);
+ if (status == -1) {
+ if (errno == EINTR)
+ continue;
+ if (errno == ECHILD) {
+ *wait_pid = (pid_t)0;
+ return STATE_ECHILD;
+ }
+ printf("do_wait/%d: EXITING, ERROR: "
+ "waitpid() returned errno %d\n",
+ getpid(), errno);
+ exit(1);
+ }
+ break;
+ }
+ *wait_pid = (pid_t)status;
+
+ if (WIFEXITED(child_status)) {
+ if (WIFSIGNALED(child_status))
+ return STATE_EXITED_TSIG;
+ if (WEXITSTATUS(child_status))
+ return STATE_EXITED_ERRSTAT;
+ return STATE_EXITED;
+ }
+ if (WIFSTOPPED(child_status)) {
+ *ret_sig = WSTOPSIG(child_status);
+ return STATE_STOPPED;
+ }
+ if (WIFSIGNALED(child_status)) {
+ *ret_sig = WTERMSIG(child_status);
+ return STATE_SIGNALED;
+ }
+ return STATE_UNKNOWN;
+}
+
+int check_sigchld(void)
+{
+ int i;
+ /*
+ * The signal is asynchronous so give it some
+ * time to arrive.
+ */
+ for (i = 0; i < 10 && !got_sigchld; i++)
+ usleep(1000); /* 10 msecs */
+ for (i = 0; i < 10 && !got_sigchld; i++)
+ usleep(2000); /* 20 + 10 = 30 msecs */
+ for (i = 0; i < 10 && !got_sigchld; i++)
+ usleep(4000); /* 40 + 30 = 70 msecs */
+ for (i = 0; i < 10 && !got_sigchld; i++)
+ usleep(8000); /* 80 + 70 = 150 msecs */
+ for (i = 0; i < 10 && !got_sigchld; i++)
+ usleep(16000); /* 160 + 150 = 310 msecs */
+
+ return got_sigchld;
+}
+
+pid_t parent;
+int nforks = 10;
+int nsteps = 10000;
+
+static void sigchld(int sig, unused siginfo_t * info, unused void *arg)
+{
+ got_sigchld = 1;
+}
+
+static void child_process(void)
+{
+ unused volatile int i;
+
+ /* wait for ptrace attach */
+ usleep(100000);
+ while (1)
+ i = 0;
+}
+
+static int forktests(int testid)
+{
+ int i, status, ret_sig;
+ long pstatus;
+ pid_t child, wait_pid;
+ struct sigaction act, oact;
+
+ parent = getpid();
+ printf("forktest#%d/%d: STARTING\n", testid, parent);
+
+ child = fork();
+ if (child == -1) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "fork returned errno %d\n", testid, parent, errno);
+ exit(1);
+ }
+ if (!child)
+ child_process();
+
+ act.sa_sigaction = sigchld;
+ sigemptyset(&act.sa_mask);
+ act.sa_flags = SA_SIGINFO;
+ status = sigaction(SIGCHLD, &act, &oact);
+ if (status) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "sigaction returned %d, errno %d\n",
+ testid, parent, status, errno);
+ exit(1);
+ }
+
+ /* give both our child and parent time to set things up */
+ usleep(125000);
+
+ /*
+ * Attach to the child.
+ */
+ pstatus = ptrace(PTRACE_ATTACH, child, NULL, NULL);
+ if (pstatus == ~0l) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "attach failed. errno %d\n",
+ testid, getpid(), errno);
+ exit(1);
+ }
+
+ /*
+ * The attach should cause the child to receive a signal.
+ */
+ status = do_wait(&wait_pid, &ret_sig);
+ if (wait_pid != child) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "attach: Unexpected wait pid %d\n",
+ testid, getpid(), wait_pid);
+ exit(1);
+ }
+ if (status != STATE_STOPPED) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "attach: wait on PTRACE_ATTACH returned %d "
+ "[%s, wanted STATE_STOPPED], signo %d\n",
+ testid, getpid(), status, get_state_name(status),
+ ret_sig);
+ exit(1);
+ }
+ else if (!check_sigchld()) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "wait on PTRACE_ATTACH saw a SIGCHLD count of %d, should be 1\n",
+ testid, getpid(), got_sigchld);
+ exit(1);
+ }
+ got_sigchld = 0;
+
+
+ /*
+ * Generate 'nsteps' PTRACE_SINGLESTEPs, make sure they all actually
+ * step the tracee.
+ */
+ for (i = 0; i < nsteps; i++) {
+ pstatus = ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
+
+ if (pstatus) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "PTRACE_SINGLESTEP #%d: returned status %ld, "
+ "errno %d, signo %d\n",
+ testid, getpid(), i, pstatus, errno, ret_sig);
+ exit(1);
+ }
+
+ status = do_wait(&wait_pid, &ret_sig);
+ if (wait_pid != child) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "wait on PTRACE_SINGLESTEP #%d: returned wrong pid %d, "
+ "expected %d\n",
+ testid, getpid(), i, wait_pid, child);
+ exit(1);
+ }
+ if (status != STATE_STOPPED) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "wait on PTRACE_SINGLESTEP #%d: wanted STATE_STOPPED, "
+ "saw %s instead (and saw signo %d too)\n",
+ testid, getpid(), i,
+ get_state_name(status), ret_sig);
+ exit(1);
+ }
+ if (ret_sig != SIGTRAP) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "wait on PTRACE_SINGLESTEP #%d: returned signal %d, "
+ "wanted SIGTRAP\n",
+ testid, getpid(), i, ret_sig);
+ exit(1);
+ }
+ if (!check_sigchld()) {
+ printf("forktest#%d/%d: EXITING, ERROR: "
+ "wait on PTRACE_SINGLESTEP #%d: no SIGCHLD seen "
+ "(signal count == 0), signo %d\n",
+ testid, getpid(), i, ret_sig);
+ exit(1);
+ }
+ got_sigchld = 0;
+ }
+
+ /* There is no need for the tracer to kill the tracee. It will
+ * automatically exit when its owner, ie, us, exits.
+ */
+
+ printf("forktest#%d/%d: EXITING, no error\n", testid, parent);
+ exit(0);
+}
+
+int main(int argc, char **argv)
+{
+ int i, ret_sig, status;
+ pid_t child = 0, wait_pid;
+ int error = 0;
+
+ setbuf(stdout, NULL);
+
+ argc--, argv++;
+ if (argc) {
+ nforks = atoi(*argv);
+ argc--, argv++;
+ if (argc)
+ nsteps = atoi(*argv);
+ }
+ printf("#forks: %d\n", nforks);
+ printf("#steps: %d\n", nsteps);
+ printf("\n");
+
+ for (i = 0; i < nforks; i++) {
+ child = fork();
+ if (child == -1) {
+ printf("main: fork returned errno %d\n", errno);
+ exit(1);
+ }
+ if (!child)
+ forktests(i);
+ }
+
+ for (i = 0; i < nforks; i++) {
+ status = do_wait(&wait_pid, &ret_sig);
+ if (status != STATE_EXITED) {
+ if (0) printf("main/%d: ERROR: "
+ "forktest#%d unexpected do_wait status %d "
+ "[%s, wanted STATE_EXITED]\n",
+ getpid(), wait_pid, status,
+ get_state_name(status));
+ error = 1;
+ }
+ }
+
+ printf("%s.\n", error ?
+ "One or more tests FAILED" :
+ "All tests PASSED");
+ exit(error);
+}
--
2.20.1