quark-test.c (1,033 lines of code) (raw):

// SPDX-License-Identifier: Apache-2.0 /* Copyright (c) 2024 Elastic NV */ #include <sys/mman.h> #include <sys/select.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/wait.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include <assert.h> #include <dirent.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <ifaddrs.h> #include <limits.h> #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <time.h> #include <unistd.h> #include "quark.h" /* For bypass tests */ #include "elastic-ebpf/GPL/Events/EbpfEventProto.h" #define MAN_QUARK_TEST #include "manpages.h" #define PATTERN "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" struct udphdr { u16 source; u16 dest; u16 len; u16 check; }; #define msleep(_x) usleep((uint64_t)_x * 1000ULL) enum { SANE, RED, GREEN, YELLOW }; static int noforkflag; /* don't fork on each test */ static int bflag; /* run bpf tests */ static int kflag; /* run kprobe tests */ static int fancy_tty(void) { char *term = getenv("TERM"); if (term == NULL || !strcmp(term, "dumb")) return (0); return (isatty(STDOUT_FILENO) == 1); } static int color(int color) { static int old; int ret; if (!fancy_tty()) return (SANE); ret = old; switch (color) { case SANE: printf("\033[0m"); break; case RED: printf("\033[31m"); break; case GREEN: printf("\033[32m"); break; case YELLOW: printf("\033[33m"); break; default: errx(1, "bad color %d", color); } old = color; return (ret); } static char * binpath(void) { static char name[PATH_MAX]; if (readlink("/proc/self/exe", name, sizeof(name)) == -1) err(1, "readlink"); return name; } static int backend_of_attr(struct quark_queue_attr *qa) { int be; if (((qa->flags & QQ_ALL_BACKENDS) == QQ_ALL_BACKENDS)) errx(1, "backend must be explicit"); else if (qa->flags & QQ_EBPF) be = QQ_EBPF; else if (qa->flags & QQ_KPROBE) be = QQ_KPROBE; else errx(1, "bad flags"); return (be); } static void spin(void) { static int ch; if (!fancy_tty()) return; /* -\|/ */ switch (ch) { case 0: /* FALLTHROUGH */ case '-': ch = '\\'; break; case '\\': ch = '|'; break; case '|': ch = '/'; break; case '/': ch = '-'; break; default: ch = '?'; } printf("%c\b", ch); fflush(stdout); } static u32 sproc_self_namespace(const char *path) { const char *errstr; char buf[512], *start, *end; ssize_t n; u32 v; int dfd; if ((dfd = open("/proc/self", O_PATH)) == -1) err(1, "open /proc/self"); n = qreadlinkat(dfd, path, buf, sizeof(buf)); close(dfd); if (n == -1) err(1, "qreadlinkat %s", path); else if (n >= (ssize_t)sizeof(buf)) errx(1, "qreadlinkat %s truncation", path); if ((start = strchr(buf, '[')) == NULL) errx(1, "no ["); if ((end = strchr(buf, ']')) == NULL) errx(1, "no ]"); start++; *end = 0; v = strtonum(start, 0, UINT32_MAX, &errstr); if (errstr != NULL) errx(1, "strtonum %s: %s", start, errstr); return (v); } static int num_open_fd(void) { DIR *dirp; struct dirent *d; int n; if ((dirp = opendir("/proc/self/fd")) == NULL) err(1, "opendir"); for (n = 0; (d = readdir(dirp)) != NULL;) { if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) continue; n++; } /* Has to be at least one, since opendir does open */ assert(n >= 1); closedir(dirp); /* Discount the FD from dirp */ return (n - 1); } static void dump_open_fd(FILE *f) { int dfd; DIR *dirp; struct dirent *d; ssize_t n; char self[512], buf[512]; if ((dfd = open("/proc/self/fd", O_DIRECTORY)) == -1) err(1, "open /proc/self/fd"); if ((dirp = fdopendir(dfd)) == NULL) err(1, "fdopendir"); snprintf(self, sizeof(self), "/proc/%d/fd", getpid()); for (n = 0; (d = readdir(dirp)) != NULL;) { if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, "..")) continue; n = qreadlinkat(dfd, d->d_name, buf, sizeof(buf)); if (n == -1) err(1, "qreadlinkat"); if (!strcmp(buf, self)) continue; fprintf(f, "%s -> %s\n", d->d_name, buf); } closedir(dirp); /* closes dfd */ fflush(f); } struct test { char *name; int (*func)(const struct test *, struct quark_queue_attr *); int backend; int excluded; }; static void display_version(void) { printf("%s-%s\n", program_invocation_short_name, QUARK_VERSION); printf("License: Apache-2.0\n"); printf("Copyright (c) 2024 Elastic NV\n"); exit(0); } static void usage(void) { fprintf(stderr, "usage: %s -h\n", program_invocation_short_name); fprintf(stderr, "usage: %s [-1bkv] [-x test] [tests ...]\n", program_invocation_short_name); fprintf(stderr, "usage: %s -l\n", program_invocation_short_name); fprintf(stderr, "usage: %s -N\n", program_invocation_short_name); fprintf(stderr, "usage: %s -V\n", program_invocation_short_name); exit(1); } static pid_t fork_exec_nop(void) { pid_t child; int status; char *const argv[] = { binpath(), "-N", "this", "is", "nop!", NULL }; if ((child = fork()) == -1) err(1, "fork"); else if (child == 0) { /* child */ return (execv(binpath(), argv)); } /* parent */ if (waitpid(child, &status, 0) == -1) err(1, "waitpid"); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) errx(1, "child didn't exit cleanly"); return (child); } static int clone_start(void *nada) { _exit(0); /* NOTREACHED */ return (0); } #define STACK_SIZE (1024UL * 128UL) static pid_t fork_clone_and_exit(void) { int flags; u8 *stack, *stack_start; pid_t pid; /* * First we do a normal fork, in this new process we will clone a new * thread and call exit. */ if ((pid = fork()) == -1) err(1, "fork"); /* parent just returns */ if (pid != 0) return (pid); /* continue on child ... */ /* * Set up a stack, clone() is like fork and we just give it a stack * without a starting addr. */ stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); if (stack == MAP_FAILED) err(1, "mmap"); stack_start = stack + STACK_SIZE; flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD | CLONE_SYSVSEM; /* * clone the new thread, pid is tid */ pid = clone(clone_start, stack_start, flags, NULL); if (pid == -1) err(1, "clone3"); /* Wait for the clone thread to exit */ for (;;) sleep(1); /* NOTREACHED */ if (munmap(stack, STACK_SIZE) == -1) err(1, "munmap"); return 0; } #undef STACK_SIZE static const struct quark_event * drain_for_pid(struct quark_queue *qq, pid_t pid) { const struct quark_event *qev; struct timespec start, now; qev = NULL; if (clock_gettime(CLOCK_MONOTONIC, &start) == -1) err(1, "clock_gettime"); now = start; for (; ; (void)clock_gettime(CLOCK_MONOTONIC, &now)) { if ((now.tv_sec - start.tv_sec) >= 5) { errno = ETIME; err(1, "drain_for_pid"); } qev = quark_queue_get_event(qq); if (qev == NULL) { if (quark_queue_block(qq) == -1) err(1, "quark_queue_block"); continue; } if (pid == -1) break; if (qev->process == NULL) continue; if (qev->process->pid != (u32)pid) continue; break; } return (qev); } static void assert_localhost(void) { struct ifaddrs *ifa; if (getifaddrs(&ifa) == -1) err(1, "getifaddrs"); if (ifa == NULL) errx(1, "getifaddrs: no addresses"); assert(!strcmp(ifa->ifa_name, "lo")); assert(ifa->ifa_addr != NULL); freeifaddrs(ifa); } static int local_listen(u16 port, int type) { struct sockaddr_in sin; int fd; bzero(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = ntohs(port); if (inet_aton("127.0.0.1", &sin.sin_addr) != 1) errx(1, "inet_aton"); if ((fd = socket(sin.sin_family, type, 0)) == -1) err(1, "socket"); if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) == -1) err(1, "bind"); if (type == SOCK_STREAM && listen(fd, 32) == -1) err(1, "listen"); return (fd); } static int local_connect(u16 port, int type, u16 *bound_port) { struct sockaddr_in sin; int fd; bzero(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = ntohs(port); if (inet_aton("127.0.0.1", &sin.sin_addr) != 1) errx(1, "inet_aton"); if ((fd = socket(sin.sin_family, type, 0)) == -1) err(1, "socket"); if (connect(fd, (struct sockaddr *)&sin, sizeof(sin)) == -1) err(1, "connect"); if (bound_port != NULL) { socklen_t socklen; socklen = sizeof(sin); if (getsockname(fd, (struct sockaddr *)&sin, &socklen) == -1) err(1, "getsockname"); *bound_port = sin.sin_port; } return (fd); } static pid_t fork_sock_write(u16 port, int type, u16 *bound_port) { pid_t child; int status, listen_fd, conn_fd; ssize_t n; int pipefd[2]; assert_localhost(); /* * We do the connect in the child, we use a pipe to send the bound port * up to us. */ if (bound_port != NULL && pipe(pipefd) == -1) err(1, "pipe"); if ((child = fork()) == -1) { err(1, "fork"); } else if (child == 0) { /* child */ listen_fd = local_listen(port, type); conn_fd = local_connect(port, type, bound_port); n = qwrite(conn_fd, PATTERN, strlen(PATTERN)); if (n == -1) err(1, "qwrite"); close(listen_fd); close(conn_fd); if (bound_port != NULL) { close(pipefd[0]); n = qwrite(pipefd[1], bound_port, sizeof(*bound_port)); if (n == -1) err(1, "qwrite"); close(pipefd[1]); } exit(0); } /* parent */ if (waitpid(child, &status, 0) == -1) err(1, "waitpid"); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) errx(1, "child didn't exit cleanly"); if (bound_port != NULL) { close(pipefd[1]); n = qread(pipefd[0], bound_port, sizeof(*bound_port)); if (n == -1) err(1, "qread"); else if (n == 0) err(1, "qread unexpected eof"); else if (n != sizeof(*bound_port)) errx(1, "qread short buf"); close(pipefd[0]); } return (child); } static int t_probe(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; if (quark_queue_open(&qq, qa) != 0) err(1, "%s: quark_queue_open", t->name); quark_queue_close(&qq); return (0); } static int t_fork_exec_exit(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_event *qev; const struct quark_process *qp; pid_t child; struct quark_cmdline_iter qcmdi; const char *arg; size_t expected_args_len; int argc; char cwd[PATH_MAX]; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); child = fork_exec_nop(); qev = drain_for_pid(&qq, child); /* check qev.events */ assert(qev->events & QUARK_EV_FORK); assert(qev->events & QUARK_EV_EXEC); assert(qev->events & QUARK_EV_EXIT); /* check qev.process */ qp = qev->process; assert(qp != NULL); assert(qp->flags & QUARK_F_EXIT); assert(qp->flags & QUARK_F_COMM); assert(qp->flags & QUARK_F_FILENAME); assert(qp->flags & QUARK_F_CMDLINE); assert(qp->flags & QUARK_F_CWD); assert((pid_t)qp->pid == child); assert((pid_t)qp->proc_ppid == getpid()); assert(qp->proc_time_boot > 0); /* XXX: improve */ assert(qp->proc_uid == getuid()); assert(qp->proc_gid == getgid()); assert(qp->proc_suid == geteuid()); assert(qp->proc_sgid == getegid()); assert(qp->proc_euid == geteuid()); assert(qp->proc_egid == getegid()); assert((pid_t)qp->proc_pgid == getpgid(0)); assert((pid_t)qp->proc_sid == getsid(0)); /* check capabilities */ /* XXX: assumes too much */ /* Newer kernels default to 0x800000000 */ assert(qp->proc_cap_inheritable == 0 || qp->proc_cap_inheritable == 0x800000000); /* * We don't know the exact set since it varies from kernel, * improve this in the future. */ assert(qp->proc_cap_effective != 0); assert(qp->proc_cap_permitted != 0); /* check entry leader */ /* * XXX TODO This depends how we're running the test, if we're over ssh * it will show ssh, if not it will show init and whatnot, for now * assert that it is not unknown at least. */ assert(qp->proc_entry_leader != 0); assert(qp->proc_entry_leader_type != QUARK_ELT_UNKNOWN); /* XXX TODO check tty_major and tty_minor for self in the future */ #if 0 assert(qp->proc_tty_major != QUARK_TTY_UNKNOWN); assert(qp->proc_tty_minor != 0); #endif /* check strings */ assert(!strcmp(qp->comm, program_invocation_short_name)); assert(!strcmp(qp->filename, binpath())); /* check args */ quark_cmdline_iter_init(&qcmdi, qp->cmdline, qp->cmdline_len); argc = 0; expected_args_len = 0; while ((arg = quark_cmdline_iter_next(&qcmdi)) != NULL) { /* * Expected len is the length of the arguments summed up, plus one byte * for each argument(the NUL after each argument, including the last * one), so we just start at 'argc' bytes. */ expected_args_len += strlen(arg) + 1; switch (argc) { case 0: assert(!strcmp(arg, binpath())); break; case 1: assert(!strcmp(arg, "-N")); break; case 2: assert(!strcmp(arg, "this")); break; case 3: assert(!strcmp(arg, "is")); break; case 4: assert(!strcmp(arg, "nop!")); break; default: errx(1, "unexpected argc"); } argc++; } assert(argc == 5); assert(qp->cmdline_len == expected_args_len); if (getcwd(cwd, sizeof(cwd)) == NULL) err(1, "getcwd"); assert(!strcmp(cwd, qp->cwd)); quark_queue_close(&qq); return (0); } /* Make sure an exit comes from tgid, not tid */ static int t_exit_tgid(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_event *qev; pid_t pid; int i; /* More aggressive since we loop */ qa->hold_time = 10; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); /* * The actual tid is not reliable, sometimes an EBPF event would come * with tid==tgid, so try it a few times */ for (i = 0; i < 20; i++) { pid = fork_clone_and_exit(); qev = drain_for_pid(&qq, pid); assert(qev->process != NULL); assert(qev->events & QUARK_EV_FORK); assert(qev->events & QUARK_EV_EXIT); } quark_queue_close(&qq); return (0); } static int t_bypass(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_event *qev; struct timespec start, now; u64 wanted; const struct ebpf_event_header *eh; qa->flags &= ~QQ_ENTRY_LEADER; qa->flags |= QQ_BYPASS; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); (void)fork_exec_nop(); wanted = EBPF_EVENT_PROCESS_FORK | EBPF_EVENT_PROCESS_EXEC | EBPF_EVENT_PROCESS_EXIT; if (clock_gettime(CLOCK_MONOTONIC, &start) == -1) err(1, "clock_gettime"); for (now = start; wanted != 0; (void)clock_gettime(CLOCK_MONOTONIC, &now)) { if ((now.tv_sec - start.tv_sec) >= 5) { errno = ETIME; err(1, "bypass"); } qev = drain_for_pid(&qq, -1); assert(qev->events == QUARK_EV_BYPASS); assert(qev->bypass != NULL); eh = qev->bypass; wanted &= ~eh->type; } quark_queue_close(&qq); return (0); } /* XXX Only probe loading for now */ static int t_file(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; qa->flags &= ~QQ_ENTRY_LEADER; qa->flags |= QQ_BYPASS | QQ_FILE; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); quark_queue_close(&qq); return (0); } static int t_sock_conn(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_event *qev; pid_t child; u16 bound_port; qa->flags |= QQ_SOCK_CONN; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); child = fork_sock_write(18888, SOCK_STREAM, &bound_port); /* QUARK_EV_FORK */ qev = drain_for_pid(&qq, child); assert(qev->events == QUARK_EV_FORK); /* SOCK_CONN_ESTABLISHED */ qev = drain_for_pid(&qq, child); assert(qev->events == QUARK_EV_SOCK_CONN_ESTABLISHED); assert(qev->process != NULL); assert((pid_t)qev->process->pid == child); assert(qev->socket != NULL); assert(qev->socket->established_time > 0); assert(qev->socket->from_scrape == 0); assert((pid_t)qev->socket->pid_origin == child); assert((pid_t)qev->socket->pid_last_use == child); assert(qev->socket->local.af == AF_INET); assert(qev->socket->local.addr4 == htonl(INADDR_LOOPBACK)); assert(qev->socket->local.port == bound_port); assert(qev->socket->remote.af == AF_INET); assert(qev->socket->remote.addr4 == htonl(INADDR_LOOPBACK)); assert(qev->socket->remote.port == htons(18888)); assert(qev->socket->close_time == 0); /* SOCK_CONN_CLOSED */ qev = drain_for_pid(&qq, child); assert(qev->events == QUARK_EV_SOCK_CONN_CLOSED); assert(qev->process != NULL); assert((pid_t)qev->process->pid == child); assert(qev->socket != NULL); assert(qev->socket->established_time > 0); assert(qev->socket->from_scrape == 0); assert((pid_t)qev->socket->pid_origin == child); assert((pid_t)qev->socket->pid_last_use == child); assert(qev->socket->local.af == AF_INET); assert(qev->socket->local.addr4 == htonl(INADDR_LOOPBACK)); assert(qev->socket->local.port == bound_port); assert(qev->socket->remote.af == AF_INET); assert(qev->socket->remote.addr4 == htonl(INADDR_LOOPBACK)); assert(qev->socket->remote.port == htons(18888)); assert(qev->socket->close_time > 0); /* QUARK_EV_EXIT */ qev = drain_for_pid(&qq, child); assert(qev->events == QUARK_EV_EXIT); quark_queue_close(&qq); return (0); } static int t_namespace(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_event *qev; pid_t child; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); child = fork_exec_nop(); qev = drain_for_pid(&qq, child); assert(qev->process != NULL); assert(qev->process->proc_uts_inonum == sproc_self_namespace("ns/uts")); assert(qev->process->proc_ipc_inonum == sproc_self_namespace("ns/ipc")); assert(qev->process->proc_mnt_inonum == sproc_self_namespace("ns/mnt")); assert(qev->process->proc_net_inonum == sproc_self_namespace("ns/net")); quark_queue_close(&qq); return (0); } static int t_cache_grace(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_process *qp; pid_t child; /* * Default grace time would slow down this test too much */ qa->cache_grace_time = 100; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); /* * Check that we ourselves exist before getting events, meaning we came * from /proc scraping */ qp = quark_process_lookup(&qq, getpid()); assert(qp != NULL); assert((pid_t)qp->pid == getpid()); /* * Fork a child, drain until we see it. */ child = fork_exec_nop(); (void)drain_for_pid(&qq, child); /* Must be in cache now */ qp = quark_process_lookup(&qq, child); assert(qp != NULL); assert((pid_t)qp->pid == child); /* * Wait the configured cache_grace_time, run a dummy get_event to * trigger the removal, ensure child is gone. */ msleep(qa->cache_grace_time); (void)quark_queue_get_event(&qq); assert(quark_process_lookup(&qq, child) == NULL); quark_queue_close(&qq); return (0); } static int t_min_agg(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_event *qev; const struct quark_process *qp; pid_t child; qa->flags |= QQ_MIN_AGG; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); /* * Fork a child, since there is no aggregation, we should see 3 events * for the same pid: FORK + EXEC + EXIT. */ child = fork_exec_nop(); /* Fork */ qev = drain_for_pid(&qq, child); assert(qev->events & QUARK_EV_FORK); assert(!(qev->events & (QUARK_EV_EXEC|QUARK_EV_EXIT))); qp = qev->process; assert(qp != NULL); assert((pid_t)qp->pid == child); assert(qp->flags & QUARK_F_PROC); /* Exec */ qev = drain_for_pid(&qq, child); assert(qev->events & QUARK_EV_EXEC); assert(!(qev->events & (QUARK_EV_FORK|QUARK_EV_EXIT))); assert((pid_t)qp->pid == child); /* Exit */ qev = drain_for_pid(&qq, child); assert(qev->events & QUARK_EV_EXIT); assert(!(qev->events & (QUARK_EV_FORK|QUARK_EV_EXEC))); qp = qev->process; assert(qp != NULL); assert((pid_t)qp->pid == child); assert(qp->flags & QUARK_F_EXIT); assert(qp->exit_code == 0); assert(qp->exit_time_event > 0); quark_queue_close(&qq); return (0); } static int t_stats(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; pid_t child; struct quark_queue_stats old_stats, stats; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); quark_queue_get_stats(&qq, &old_stats); assert(old_stats.backend == backend_of_attr(qa)); assert(old_stats.insertions == 0); assert(old_stats.removals == 0); assert(old_stats.aggregations == 0); assert(old_stats.non_aggregations == 0); assert(old_stats.lost == 0); /* * Fork a child, drain until we see it. */ child = fork_exec_nop(); (void)drain_for_pid(&qq, child); /* * Stats must have bumped now */ quark_queue_get_stats(&qq, &stats); assert(stats.backend == old_stats.backend); assert(stats.insertions > old_stats.insertions); assert(stats.removals > old_stats.removals); assert(stats.aggregations > old_stats.aggregations); /* Can't state anything about non_aggregations */ /* If we're losing here, all hope is lost */ assert(old_stats.lost == 0); /* XXX We should trigger lost events and ensure here XXX */ quark_queue_close(&qq); return (0); } static int t_dns(const struct test *t, struct quark_queue_attr *qa) { struct quark_queue qq; const struct quark_event *qev; struct quark_packet *packet; pid_t child; struct iphdr ip; struct udphdr udp; u16 bound_port; assert_localhost(); qa->flags |= QQ_DNS; if (quark_queue_open(&qq, qa) != 0) err(1, "quark_queue_open"); child = fork_sock_write(53, SOCK_DGRAM, &bound_port); /* first is the fork, no agg */ qev = drain_for_pid(&qq, child); assert(qev->packet == NULL); /* egress */ qev = drain_for_pid(&qq, child); packet = qev->packet; assert(packet != NULL); assert(packet->cap_len == 90); assert(packet->orig_len == 90); /* ip */ memcpy(&ip, packet->data, sizeof(ip)); assert(ip.protocol == IPPROTO_UDP); assert(ip.saddr == htonl(INADDR_LOOPBACK)); assert(ip.daddr == htonl(INADDR_LOOPBACK)); /* udp */ memcpy(&udp, packet->data + sizeof(ip), sizeof(udp)); assert(udp.dest == htons(53)); assert(udp.source == bound_port); /* dns */ assert(!memcmp(packet->data + 28, PATTERN, packet->cap_len - 28)); /* ingress */ qev = drain_for_pid(&qq, child); packet = qev->packet; assert(packet != NULL); assert(packet->cap_len == 90); assert(packet->orig_len == 90); /* ip */ memcpy(&ip, packet->data, sizeof(ip)); assert(ip.protocol == IPPROTO_UDP); assert(ip.saddr == htonl(INADDR_LOOPBACK)); assert(ip.daddr == htonl(INADDR_LOOPBACK)); /* udp */ memcpy(&udp, packet->data + sizeof(ip), sizeof(udp)); assert(udp.dest == htons(53)); assert(udp.source == bound_port); /* dns */ assert(!memcmp(packet->data + 28, PATTERN, packet->cap_len - 28)); quark_queue_close(&qq); return (0); } /* * Try to order by increasing order of complexity */ #define T(_x) { S(_x), _x, QQ_ALL_BACKENDS, 0 } #define T_KPROBE(_x) { S(_x), _x, QQ_KPROBE, 0 } #define T_EBPF(_x) { S(_x), _x, QQ_EBPF, 0 } #define S(_x) #_x struct test all_tests[] = { T(t_probe), T(t_fork_exec_exit), T(t_exit_tgid), T_EBPF(t_bypass), T_EBPF(t_file), T_EBPF(t_sock_conn), T_EBPF(t_dns), T(t_namespace), T(t_cache_grace), T(t_min_agg), T(t_stats), { NULL, NULL, 0, 0 } }; #undef S #undef T static void display_tests(void) { const struct test *t; for (t = all_tests; t->name != NULL; t++) printf("%s\n", t->name); exit(0); } static struct test * lookup_test(const char *name) { struct test *t; for (t = all_tests; t->name != NULL; t++) { if (!strcmp(t->name, name)) return (t); } return (NULL); } static int run_test_doit(struct test *t, struct quark_queue_attr *qa) { int nfd, r; struct quark_queue_attr qa_copy; /* * Check for FD leaks */ nfd = num_open_fd(); assert(nfd == 3); qa_copy = *qa; r = t->func(t, &qa_copy); nfd = num_open_fd(); if (nfd != 3) { fprintf(stderr, "FDLEAK DETECTED! %d opened descriptors, expected 3\n", nfd); dump_open_fd(stderr); if (r == 0) r = 1; } return (r); } /* * A test runs as a subprocess to avoid contamination. */ static int run_test(struct test *t, struct quark_queue_attr *qa) { pid_t child; int status, x, linepos, be, r; int child_stderr[2]; FILE *child_stream; char *child_buf; size_t child_buflen; ssize_t n; /* * Figure out if this is ebpf or kprobe */ be = backend_of_attr(qa); if (be != QQ_EBPF && be != QQ_KPROBE) errx(1, "bad backend"); linepos = printf("%s @ %s", t->name, be == QQ_EBPF ? "ebpf" : "kprobe"); while (++linepos < 30) putchar('.'); fflush(stdout); if (((t->backend & be) == 0) || t->excluded) { x = color(YELLOW); printf("n/a\n"); color(x); fflush(stdout); return (0); } if (noforkflag) { r = run_test_doit(t, qa); if (r == 0) { x = color(GREEN); printf("ok\n"); color(x); } else { x = color(RED); printf("failed\n"); color(x); } fflush(stdout); return (r); } /* * Create a pipe to save the child stderr, so we don't get crappy * interleaved output with the parent. */ if (pipe(child_stderr) == -1) err(1, "pipe"); /* * Fork child and point its stderr to the pipe */ if ((child = fork()) == -1) err(1, "fork"); else if (child == 0) { dup2(child_stderr[1], STDERR_FILENO); close(child_stderr[1]); close(child_stderr[0]); exit(run_test_doit(t, qa)); } close(child_stderr[1]); /* * Open a write stream to save child's stderr output */ child_buf = NULL; child_buflen = 0; child_stream = open_memstream(&child_buf, &child_buflen); if (child_stream == NULL) err(1, "open_memstream"); /* * Drain the pipe until EOF, meaning the child exited */ for (;;) { fd_set rfds; struct timeval tv; char buf[4096]; spin(); tv.tv_sec = 0; tv.tv_usec = 25; FD_ZERO(&rfds); FD_SET(child_stderr[0], &rfds); r = select(child_stderr[0] + 1, &rfds, NULL, NULL, &tv); if (r == -1 && (errno == EINTR)) continue; else if (r == -1) err(1, "select"); else if (r == 0) continue; if (!FD_ISSET(child_stderr[0], &rfds)) errx(1, "rfds should be set"); n = qread(child_stderr[0], buf, sizeof(buf)); if (n == -1) err(1, "read"); else if (n == 0) { close(child_stderr[0]); break; } /* n is positive, move to the stream */ if (fwrite(buf, 1, n, child_stream) != (size_t)n) err(1, "fwrite"); if (ferror(child_stream)) err(1, "fwrite"); if (feof(child_stream)) errx(1, "fwrite got EOF"); } /* * We only get here when we get an EOF from the child pipe, so it * must have exited. */ if (waitpid(child, &status, 0) == -1) err(1, "waitpid"); if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { x = color(GREEN); printf("ok\n"); color(x); } else { x = color(RED); printf("failed\n"); color(x); } fflush(stdout); if (WIFSIGNALED(status)) fprintf(stderr, "exited with signal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status))); else if (WCOREDUMP(status)) fprintf(stderr, "core dumped\n"); /* * Children exited, close the stream and print it out. */ fclose(child_stream); n = qwrite(STDERR_FILENO, child_buf, child_buflen); if (n == -1) err(1, "qwrite"); free(child_buf); child_buf = NULL; child_buflen = 0; if (WIFEXITED(status)) return (WEXITSTATUS(status)); return (-1); } static int run_tests(int argc, char *argv[]) { struct test *t; int failed, i; struct quark_queue_attr bpf_attr; struct quark_queue_attr kprobe_attr; quark_queue_default_attr(&bpf_attr); bpf_attr.flags &= ~QQ_ALL_BACKENDS; bpf_attr.flags |= QQ_EBPF | QQ_ENTRY_LEADER; bpf_attr.hold_time = 100; quark_queue_default_attr(&kprobe_attr); kprobe_attr.flags &= ~QQ_ALL_BACKENDS; kprobe_attr.flags |= QQ_KPROBE | QQ_ENTRY_LEADER; kprobe_attr.hold_time = 100; failed = 0; if (argc == 0) { for (t = all_tests; t->name != NULL; t++) { if (bflag && run_test(t, &bpf_attr) != 0) failed++; if (kflag && run_test(t, &kprobe_attr) != 0) failed++; } } else { for (i = 0; i < argc; i++) { t = lookup_test(argv[i]); if (t == NULL) errx(1, "test %s doesn't exist", argv[i]); if (bflag && run_test(t, &bpf_attr) != 0) failed++; if (kflag && run_test(t, &kprobe_attr) != 0) failed++; } } return (failed); } int main(int argc, char *argv[]) { int ch, failed, x; struct test *t; while ((ch = getopt(argc, argv, "1bhklNvVx:")) != -1) { switch (ch) { case '1': noforkflag = 1; break; case 'b': bflag = 1; break; case 'h': if (isatty(STDOUT_FILENO)) display_man(); else usage(); break; /* NOTREACHED */ case 'k': kflag = 1; break; case 'l': display_tests(); break; /* NOTREACHED */ case 'N': exit(0); break; /* NOTREACHED */ case 'v': quark_verbose++; break; case 'V': display_version(); break; /* NOTREACHED */ case 'x': if ((t = lookup_test(optarg)) == NULL) errx(1, "test %s doesn't exist", optarg); t->excluded = 1; break; default: usage(); } } if (!bflag && !kflag) bflag = kflag = 1; argc -= optind; argv += optind; failed = run_tests(argc, argv); x = failed == 0 ? color(GREEN) : color(RED); printf("%d failures\n", failed); color(x); return (failed); }