#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum { Failtimeout = 8000, }; typedef struct Test Test; struct Test { pid_t child; int errfd; int notifyfd; int runtimewd; int buswd; int endpointclean; char root[256]; char runtime[320]; char config[320]; char ibus[384]; char bus[448]; char home[320]; char socket[384]; char badmap[320]; char expected[768]; char err[4096]; size_t nerr; }; static uint32_t endpointmask = IN_CREATE|IN_MOVED_TO|IN_MOVED_FROM|IN_ATTRIB| IN_DELETE|IN_MODIFY|IN_CLOSE_WRITE|IN_DELETE_SELF|IN_MOVE_SELF; static int fail(char *fmt, ...) { va_list ap; fprintf(stderr, "daemon_failure_test: "); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); return 0; } static int64_t nowms(void) { struct timespec ts; if(clock_gettime(CLOCK_MONOTONIC, &ts) < 0) return -1; return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } static int leftms(int64_t deadline) { int64_t n; n = nowms(); if(n < 0) return -1; n = deadline - n; if(n <= 0) return 0; if(n > 0x7fffffff) return 0x7fffffff; return n; } static int makedir(char *path) { if(mkdir(path, 0700) == 0) return 1; return fail("mkdir %s: %s", path, strerror(errno)); } static void readerrors(Test *t) { ssize_t n; if(t->errfd < 0) return; while(t->nerr + 1 < sizeof t->err){ n = read(t->errfd, t->err + t->nerr, sizeof t->err - t->nerr - 1); if(n > 0){ t->nerr += n; continue; } if(n < 0 && errno == EINTR) continue; break; } t->err[t->nerr] = '\0'; } static void showerrors(Test *t) { readerrors(t); if(t->nerr != 0) fprintf(stderr, "daemon_failure_test: daemon stderr:\n%s", t->err); } static int setup(Test *t) { memset(t, 0, sizeof *t); t->child = -1; t->errfd = -1; t->notifyfd = -1; t->runtimewd = -1; t->buswd = -1; t->endpointclean = 1; if(nowms() < 0) return fail("read monotonic clock: %s", strerror(errno)); snprintf(t->root, sizeof t->root, "/tmp/strans-failure.XXXXXX"); if(mkdtemp(t->root) == NULL){ t->root[0] = '\0'; return fail("mkdtemp: %s", strerror(errno)); } if(snprintf(t->runtime, sizeof t->runtime, "%s/runtime", t->root) >= (int)sizeof t->runtime || snprintf(t->config, sizeof t->config, "%s/config", t->root) >= (int)sizeof t->config || snprintf(t->ibus, sizeof t->ibus, "%s/ibus", t->config) >= (int)sizeof t->ibus || snprintf(t->bus, sizeof t->bus, "%s/bus", t->ibus) >= (int)sizeof t->bus || snprintf(t->home, sizeof t->home, "%s/home", t->root) >= (int)sizeof t->home || snprintf(t->socket, sizeof t->socket, "%s/strans.sock", t->runtime) >= (int)sizeof t->socket || snprintf(t->badmap, sizeof t->badmap, "%s/missing-map", t->root) >= (int)sizeof t->badmap || snprintf(t->expected, sizeof t->expected, "strans: can't open: %s/hira.map\n", t->badmap) >= (int)sizeof t->expected) return fail("temporary path is too long"); if(!makedir(t->runtime) || !makedir(t->config) || !makedir(t->ibus) || !makedir(t->bus) || !makedir(t->home)) return 0; t->notifyfd = inotify_init1(IN_CLOEXEC|IN_NONBLOCK); if(t->notifyfd < 0) return fail("inotify_init1: %s", strerror(errno)); t->runtimewd = inotify_add_watch(t->notifyfd, t->runtime, endpointmask); if(t->runtimewd < 0) return fail("watch runtime directory: %s", strerror(errno)); t->buswd = inotify_add_watch(t->notifyfd, t->bus, endpointmask); if(t->buswd < 0) return fail("watch IBus directory: %s", strerror(errno)); return 1; } static int startchild(Test *t, char *program) { int errpipe[2], fd; long maxfd; pid_t pid; if(pipe2(errpipe, O_CLOEXEC|O_NONBLOCK) < 0) return fail("pipe2: %s", strerror(errno)); pid = fork(); if(pid < 0){ close(errpipe[0]); close(errpipe[1]); return fail("fork: %s", strerror(errno)); } if(pid == 0){ close(errpipe[0]); if(dup2(errpipe[1], STDERR_FILENO) < 0) _exit(126); close(errpipe[1]); if(close_range(3, UINT_MAX, 0) < 0){ maxfd = sysconf(_SC_OPEN_MAX); if(maxfd < 0) maxfd = 1024; for(fd = 3; fd < maxfd; fd++) close(fd); } if(setenv("XDG_RUNTIME_DIR", t->runtime, 1) < 0 || setenv("XDG_CONFIG_HOME", t->config, 1) < 0 || setenv("HOME", t->home, 1) < 0 || unsetenv("DISPLAY") < 0 || unsetenv("DBUS_SESSION_BUS_ADDRESS") < 0 || unsetenv("IBUS_ADDRESS") < 0){ dprintf(STDERR_FILENO, "set daemon environment: %s\n", strerror(errno)); _exit(126); } execl(program, program, t->badmap, (char*)0); dprintf(STDERR_FILENO, "exec %s: %s\n", program, strerror(errno)); _exit(127); } close(errpipe[1]); t->child = pid; t->errfd = errpipe[0]; return 1; } static int drainnotify(Test *t) { char buf[4096]; char *where; struct inotify_event *ev; ssize_t n; size_t off; int ok; ok = 1; for(;;){ n = read(t->notifyfd, buf, sizeof buf); if(n < 0 && errno == EINTR) continue; if(n < 0 && errno == EAGAIN) return ok; if(n < 0) return fail("read endpoint watches: %s", strerror(errno)); if(n == 0) return ok; for(off = 0; off + sizeof *ev <= (size_t)n; off += sizeof *ev + ev->len){ ev = (struct inotify_event*)(buf + off); if(off + sizeof *ev + ev->len > (size_t)n) return fail("truncated inotify event"); if(ev->mask & IN_Q_OVERFLOW){ t->endpointclean = 0; fail("endpoint watch queue overflowed"); ok = 0; continue; } if(ev->mask & (IN_IGNORED|IN_UNMOUNT)){ t->endpointclean = 0; fail("endpoint watch became invalid: %#x", ev->mask); ok = 0; continue; } if(ev->wd != t->runtimewd && ev->wd != t->buswd) continue; if(!(ev->mask & endpointmask)) continue; where = ev->wd == t->runtimewd ? "runtime" : "IBus"; t->endpointclean = 0; fail("unexpected %s endpoint event %#x for %s", where, ev->mask, ev->len != 0 ? ev->name : "directory"); ok = 0; } if(off != (size_t)n) return fail("truncated inotify event buffer"); } } static int killowned(Test *t, int *status) { pid_t pid; int n, ok; if(t->child <= 0) return 1; pid = t->child; ok = 1; if(kill(pid, SIGKILL) < 0 && errno != ESRCH){ fail("kill -9 %ld: %s", (long)pid, strerror(errno)); ok = 0; } do n = waitpid(pid, status, 0); while(n < 0 && errno == EINTR); if(n != pid){ fail("reap %ld after SIGKILL: %s", (long)pid, n < 0 ? strerror(errno) : "wrong child"); ok = 0; }else t->child = -1; return ok; } static int waitfailed(Test *t) { struct pollfd pfd[2]; pid_t pid; int n, ok, status, timeout; int64_t deadline; pid = t->child; ok = 1; deadline = nowms(); if(deadline < 0){ fail("read monotonic clock before waiting for daemon: %s", strerror(errno)); killowned(t, &status); readerrors(t); drainnotify(t); return 0; } deadline += Failtimeout; for(;;){ do n = waitpid(pid, &status, WNOHANG); while(n < 0 && errno == EINTR); if(n == pid){ t->child = -1; readerrors(t); if(!drainnotify(t)) ok = 0; break; } if(n < 0){ fail("waitpid failed daemon %ld: %s", (long)pid, strerror(errno)); if(errno == ECHILD){ t->child = -1; readerrors(t); drainnotify(t); }else{ killowned(t, &status); readerrors(t); drainnotify(t); } return 0; } timeout = leftms(deadline); if(timeout < 0){ fail("read monotonic clock while waiting for daemon: %s", strerror(errno)); killowned(t, &status); readerrors(t); drainnotify(t); return 0; } if(timeout == 0){ fail("daemon %ld did not exit after map initialization failure", (long)pid); killowned(t, &status); return 0; } pfd[0].fd = t->notifyfd; pfd[0].events = POLLIN; pfd[0].revents = 0; pfd[1].fd = t->errfd; pfd[1].events = POLLIN|POLLHUP; pfd[1].revents = 0; n = poll(pfd, 2, timeout); if(n < 0 && errno == EINTR) continue; if(n < 0){ fail("poll failed daemon: %s", strerror(errno)); killowned(t, &status); readerrors(t); drainnotify(t); return 0; } if(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)){ fail("endpoint watch became unusable: %#x", pfd[0].revents); killowned(t, &status); readerrors(t); drainnotify(t); return 0; } if(pfd[1].revents & (POLLERR|POLLNVAL)){ fail("daemon stderr pipe became unusable: %#x", pfd[1].revents); killowned(t, &status); readerrors(t); drainnotify(t); return 0; } if((pfd[0].revents & POLLIN) && !drainnotify(t)) ok = 0; if(pfd[1].revents & (POLLIN|POLLHUP)) readerrors(t); } if(!WIFEXITED(status) || WEXITSTATUS(status) != 1) return fail("daemon map failure had wait status %#x", status); if(strcmp(t->err, t->expected) != 0) return fail("daemon did not report the expected map initialization failure"); return ok && t->endpointclean; } static int dirempty(char *path, char *which) { DIR *dir; struct dirent *de; int count, ok; dir = opendir(path); if(dir == NULL) return fail("open %s directory %s: %s", which, path, strerror(errno)); count = 0; ok = 1; errno = 0; while((de = readdir(dir)) != NULL){ if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; count++; fail("unexpected %s directory entry %s", which, de->d_name); ok = 0; errno = 0; } if(errno != 0){ fail("read %s directory %s: %s", which, path, strerror(errno)); ok = 0; } if(closedir(dir) < 0){ fail("close %s directory %s: %s", which, path, strerror(errno)); ok = 0; } if(count != 0) ok = 0; return ok; } static int checkendpoints(Test *t) { struct stat st; int ok; ok = 1; errno = 0; if(lstat(t->socket, &st) == 0){ fail("IPC endpoint exists after failed startup: %s", t->socket); ok = 0; }else if(errno != ENOENT){ fail("lstat failed IPC endpoint: %s", strerror(errno)); ok = 0; } if(!dirempty(t->runtime, "runtime")) ok = 0; if(!dirempty(t->bus, "IBus")) ok = 0; return ok && t->endpointclean; } static int clearfd(int); static int removeentry(int dirfd, char *name) { struct stat st; int fd, ok; if(fstatat(dirfd, name, &st, AT_SYMLINK_NOFOLLOW) < 0) return fail("stat cleanup entry %s: %s", name, strerror(errno)); if(!S_ISDIR(st.st_mode)){ if(unlinkat(dirfd, name, 0) == 0 || errno == ENOENT) return 1; return fail("remove cleanup entry %s: %s", name, strerror(errno)); } fd = openat(dirfd, name, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); if(fd < 0) return fail("open cleanup directory %s: %s", name, strerror(errno)); ok = clearfd(fd); if(close(fd) < 0){ fail("close cleanup directory %s: %s", name, strerror(errno)); ok = 0; } if(unlinkat(dirfd, name, AT_REMOVEDIR) < 0 && errno != ENOENT){ fail("remove cleanup directory %s: %s", name, strerror(errno)); ok = 0; } return ok; } static int clearfd(int fd) { DIR *dir; struct dirent *de; int copy, ok; copy = dup(fd); if(copy < 0) return fail("duplicate cleanup directory: %s", strerror(errno)); dir = fdopendir(copy); if(dir == NULL){ close(copy); return fail("open cleanup directory stream: %s", strerror(errno)); } ok = 1; errno = 0; while((de = readdir(dir)) != NULL){ if(strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue; if(!removeentry(fd, de->d_name)) ok = 0; errno = 0; } if(errno != 0){ fail("read cleanup directory: %s", strerror(errno)); ok = 0; } if(closedir(dir) < 0){ fail("close cleanup directory stream: %s", strerror(errno)); ok = 0; } return ok; } static int removeroot(Test *t) { int fd, ok; if(t->root[0] == '\0') return 1; fd = open(t->root, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); if(fd < 0) return errno == ENOENT || fail("open cleanup root %s: %s", t->root, strerror(errno)); ok = clearfd(fd); if(close(fd) < 0){ fail("close cleanup root %s: %s", t->root, strerror(errno)); ok = 0; } if(rmdir(t->root) < 0 && errno != ENOENT){ fail("rmdir %s: %s", t->root, strerror(errno)); ok = 0; } return ok; } static int cleanup(Test *t) { int n, ok, status; ok = 1; if(t->child > 0){ do n = waitpid(t->child, &status, WNOHANG); while(n < 0 && errno == EINTR); if(n == t->child) t->child = -1; else if(n == 0){ fail("failed-start daemon still running during cleanup"); ok = 0; if(!killowned(t, &status)) ok = 0; }else{ fail("check failed-start daemon during cleanup: %s", strerror(errno)); ok = 0; if(errno == ECHILD) t->child = -1; else if(!killowned(t, &status)) ok = 0; } } readerrors(t); if(t->errfd >= 0){ if(close(t->errfd) < 0){ fail("close daemon stderr: %s", strerror(errno)); ok = 0; } t->errfd = -1; } if(t->notifyfd >= 0){ if(t->runtimewd >= 0 && inotify_rm_watch(t->notifyfd, t->runtimewd) < 0 && errno != EINVAL){ fail("remove runtime watch: %s", strerror(errno)); ok = 0; } if(t->buswd >= 0 && inotify_rm_watch(t->notifyfd, t->buswd) < 0 && errno != EINVAL){ fail("remove IBus watch: %s", strerror(errno)); ok = 0; } if(close(t->notifyfd) < 0){ fail("close endpoint watches: %s", strerror(errno)); ok = 0; } t->notifyfd = -1; } if(!removeroot(t)) ok = 0; return ok; } int main(int argc, char **argv) { Test test; int ok; if(argc != 2){ fprintf(stderr, "usage: daemon_failure_test strans\n"); return 2; } ok = setup(&test); if(ok) ok = startchild(&test, argv[1]); if(ok){ ok = waitfailed(&test); if(test.child <= 0 && !checkendpoints(&test)) ok = 0; } if(!cleanup(&test)) ok = 0; if(!ok){ showerrors(&test); return 1; } printf("failed daemon startup published no endpoints: ok\n"); return 0; }