/* fprobe: Foundry OS's libc self-check (milestone M2). * * A small, deterministic cousin of Clarke's mprobe2 probe * (clarke-scratch/exp16..exp19/mprobe2.c): it checks what the C library * gets back from the Rustbelt kernel -- the VALUES, not just "returned 0". * Every check prints one line ending in "ok" or "FAIL"; the last line is * fprobe: all N checks passed (exit code 0), or * fprobe: F of N checks FAILED (exit code F). * Timing checks leave room for Rustbelt's ~55 ms timer tick. The * benchmark-style checks of mprobe2 (which can fail on a busy host) are * left out on purpose: `make test` must not be flaky. * * Built with toolchain/foundry-cc (static-PIE). Runs on Linux too: * toolchain/foundry-cc -O2 -pthread c/fprobe.c -o out/fprobe && out/fprobe * (a few checks are Rustbelt-specific and accept Linux's answer as well). */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int fails, checks, on_rustbelt; /* One check: print its name, then ok or FAIL (with errno when it failed). */ static void chk(const char *what, int ok) { int e = errno; checks++; if (!ok) fails++; printf(" %-46s %s", what, ok ? "ok" : "FAIL"); if (!ok) printf(" (errno %d %s)", e, strerror(e)); printf("\n"); errno = 0; } static long long now_ms(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec * 1000LL + t.tv_nsec / 1000000; } static void ms_sleep(long ms) { struct timespec t = { ms / 1000, ms % 1000 * 1000000L }; nanosleep(&t, NULL); } /* ---- the program itself: ELF, auxv ------------------------------------- */ extern const char __ehdr_start[]; static void elf_and_auxv(void) { printf("fprobe: -- ELF and auxv\n"); const Elf64_Ehdr *me = (const void *)__ehdr_start; const Elf64_Phdr *ph = (const void *)(__ehdr_start + me->e_phoff); int interp = 0; for (int i = 0; i < me->e_phnum; i++) interp |= ph[i].p_type == PT_INTERP; chk("static-PIE: ET_DYN without PT_INTERP", me->e_type == ET_DYN && !interp); chk("AT_BASE == 0 (no dynamic loader)", getauxval(AT_BASE) == 0); chk("AT_PHDR == our header + e_phoff", getauxval(AT_PHDR) == (unsigned long)__ehdr_start + me->e_phoff); chk("AT_PAGESZ 4096, AT_RANDOM present", getauxval(AT_PAGESZ) == 4096 && getauxval(AT_RANDOM) != 0); chk("vDSO mapped (AT_SYSINFO_EHDR)", getauxval(AT_SYSINFO_EHDR) != 0); } /* ---- files in /tmp ------------------------------------------------------ */ static int rm_cb(const char *p, const struct stat *s, int f, struct FTW *w) { (void)s, (void)f, (void)w; return remove(p); } static int count_dir(const char *d) { DIR *dd = opendir(d); struct dirent *e; int n = 0; if (!dd) return -1; while ((e = readdir(dd))) if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) n++; closedir(dd); return n; } static void files(void) { printf("fprobe: -- files (/tmp)\n"); const char *D = "/tmp/fprobe", *F = "/tmp/fprobe/a.txt"; struct stat st, st2; char buf[64] = { 0 }; nftw(D, rm_cb, 8, FTW_DEPTH | FTW_PHYS); /* leftovers from an earlier run */ umask(022); chk("mkdir", mkdir(D, 0755) == 0); int fd = open(F, O_CREAT | O_TRUNC | O_RDWR, 0666); chk("open O_CREAT 0666 -> mode 0644 (umask 022)", fd >= 0 && fstat(fd, &st) == 0 && (st.st_mode & 0777) == 0644); chk("write + lseek + read back", write(fd, "hello world\n", 12) == 12 && lseek(fd, 6, SEEK_SET) == 6 && read(fd, buf, sizeof buf - 1) == 6 && strcmp(buf, "world\n") == 0); chk("ftruncate -> st_size 5", ftruncate(fd, 5) == 0 && fstat(fd, &st) == 0 && st.st_size == 5); chk("mtime is a real date (after 2020)", st.st_mtime > 1600000000); close(fd); chk("rename", rename(F, "/tmp/fprobe/b.txt") == 0 && stat(F, &st) == -1 && errno == ENOENT); errno = 0; chk("link: same inode, st_nlink 2", link("/tmp/fprobe/b.txt", "/tmp/fprobe/c.txt") == 0 && stat("/tmp/fprobe/b.txt", &st) == 0 && stat("/tmp/fprobe/c.txt", &st2) == 0 && st.st_ino == st2.st_ino && st.st_nlink == 2); memset(buf, 0, sizeof buf); chk("symlink + readlink", symlink("b.txt", "/tmp/fprobe/l") == 0 && readlink("/tmp/fprobe/l", buf, sizeof buf) == 5 && strcmp(buf, "b.txt") == 0); chk("lstat sees the link, stat follows it", lstat("/tmp/fprobe/l", &st) == 0 && S_ISLNK(st.st_mode) && stat("/tmp/fprobe/l", &st2) == 0 && S_ISREG(st2.st_mode) && st2.st_size == 5); for (int i = 0; i < 20; i++) { snprintf(buf, sizeof buf, "/tmp/fprobe/f%02d", i); close(open(buf, O_CREAT | O_WRONLY, 0644)); } chk("readdir lists all 23 entries", count_dir(D) == 23); chk("nftw(FTW_DEPTH) removes the tree (rm -r)", nftw(D, rm_cb, 8, FTW_DEPTH | FTW_PHYS) == 0 && stat(D, &st) == -1); errno = 0; chk("open of a missing file -> ENOENT", open("/tmp/no/such", O_RDONLY) == -1 && errno == ENOENT); } /* ---- processes, pipes, signals ------------------------------------------ */ static volatile sig_atomic_t got; static void on_sig(int s) { got = s; } static void processes(void) { printf("fprobe: -- processes, pipes, signals\n"); int p[2], st = 0; chk("pipe", pipe(p) == 0); pid_t c = fork(); if (c == 0) { close(p[0]); dprintf(p[1], "child %d", (int)getpid()); _exit(7); } close(p[1]); char buf[32] = { 0 }; int n = read(p[0], buf, sizeof buf - 1); close(p[0]); chk("fork: child writes into the pipe", n > 6 && strncmp(buf, "child ", 6) == 0 && atoi(buf + 6) == c); chk("waitpid: exit status 7", waitpid(c, &st, 0) == c && WIFEXITED(st) && WEXITSTATUS(st) == 7); c = fork(); if (c == 0) { execl("/no/such/program", "x", (char *)NULL); _exit(errno == ENOENT ? 3 : 4); } chk("execve of a missing file -> ENOENT", waitpid(c, &st, 0) == c && WEXITSTATUS(st) == 3); c = fork(); if (c == 0) { pause(); _exit(0); } kill(c, SIGTERM); chk("kill(SIGTERM) -> WIFSIGNALED", waitpid(c, &st, 0) == c && WIFSIGNALED(st) && WTERMSIG(st) == SIGTERM); struct sigaction sa = { .sa_handler = on_sig }; sigemptyset(&sa.sa_mask); sigaction(SIGUSR1, &sa, NULL); sigset_t s, old, pend; sigemptyset(&s); sigaddset(&s, SIGUSR1); sigprocmask(SIG_BLOCK, &s, &old); got = 0; raise(SIGUSR1); sigemptyset(&pend); chk("blocked signal stays pending", got == 0 && sigpending(&pend) == 0 && sigismember(&pend, SIGUSR1)); sigprocmask(SIG_SETMASK, &old, NULL); chk("unblocking runs the handler", got == SIGUSR1); } /* ---- threads, memory ----------------------------------------------------- */ static __thread int tls = 7; static long counter; static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; static void *worker(void *arg) { int ok = tls == 7; tls = 100 + (int)(long)arg; for (int i = 0; i < 20000; i++) { pthread_mutex_lock(&lock); counter++; pthread_mutex_unlock(&lock); } return (void *)(long)(ok && tls == 100 + (int)(long)arg); } static void threads_and_memory(void) { printf("fprobe: -- threads and memory\n"); pthread_t t[4]; long all = 1; for (long i = 0; i < 4; i++) pthread_create(&t[i], NULL, worker, (void *)i); for (int i = 0; i < 4; i++) { void *r; pthread_join(t[i], &r); all &= (long)r; } chk("4 threads: own copy of __thread data", all && tls == 7); chk("4 threads x 20000 mutex increments = 80000", counter == 80000); char *m = mmap(NULL, 1 << 20, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); int zero = m != MAP_FAILED; for (int i = 0; zero && i < 1 << 20; i += 4096) zero = m[i] == 0; chk("mmap 1 MiB anonymous, zero-filled", zero); if (m != MAP_FAILED) { memset(m, 0x5a, 1 << 20); char *g = mremap(m, 1 << 20, 4 << 20, MREMAP_MAYMOVE); chk("mremap grow 1 -> 4 MiB keeps the data", g != MAP_FAILED && g[0] == 0x5a && g[(1 << 20) - 1] == 0x5a && g[(4 << 20) - 1] == 0); munmap(g != MAP_FAILED ? g : m, g != MAP_FAILED ? 4 << 20 : 1 << 20); } char *big = malloc(8 << 20), *small = malloc(100); chk("malloc 8 MiB + 100 bytes", big && small); if (big) big[(8 << 20) - 1] = 1; free(big); free(small); char out[64]; snprintf(out, sizeof out, "%.3f %s %d", strtod("2.5e1", NULL) / 8, "x", -42); chk("printf %f / strtod", strcmp(out, "3.125 x -42") == 0); } /* ---- clocks and timerfd (Rustbelt 0.20.0) -------------------------------- */ static int arm(int fd, int flags, long ms, long every_ms) { struct itimerspec its = { { every_ms / 1000, every_ms % 1000 * 1000000L }, { ms / 1000, ms % 1000 * 1000000L } }; return timerfd_settime(fd, flags, &its, NULL); } static void clocks_and_timerfd(void) { printf("fprobe: -- clocks and timerfd\n"); struct timespec a, b, rt; int back = 0; clock_gettime(CLOCK_MONOTONIC, &a); for (int i = 0; i < 100000; i++) { clock_gettime(CLOCK_MONOTONIC, &b); back += b.tv_sec < a.tv_sec || (b.tv_sec == a.tv_sec && b.tv_nsec < a.tv_nsec); a = b; } chk("100000 MONOTONIC reads never go backwards", back == 0); clock_gettime(CLOCK_REALTIME, &rt); chk("REALTIME is 2026 or later", rt.tv_sec > 1767225600L); long long t0 = now_ms(); ms_sleep(200); long long slept = now_ms() - t0; chk("nanosleep 200 ms takes 200-1000 ms", slept >= 195 && slept < 1000); /* One-shot timer, blocking read: waits ~150 ms, then the count is 1. */ int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); chk("timerfd_create(MONOTONIC)", fd >= 0); uint64_t n = 0; t0 = now_ms(); arm(fd, 0, 150, 0); int r = read(fd, &n, sizeof n); long long took = now_ms() - t0; chk("timerfd one-shot: read blocks ~150 ms, count 1", r == 8 && n == 1 && took >= 140 && took < 1000); struct itimerspec cur; chk("timerfd disarmed after firing", timerfd_gettime(fd, &cur) == 0 && cur.it_value.tv_sec == 0 && cur.it_value.tv_nsec == 0); close(fd); /* Periodic timer: expirations add up while nobody reads. */ fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK); errno = 0; chk("timerfd nonblocking + disarmed: EAGAIN", read(fd, &n, sizeof n) == -1 && errno == EAGAIN); arm(fd, 0, 60, 60); ms_sleep(700); n = 0; chk("timerfd 60 ms interval: 5-14 expirations in 700 ms", read(fd, &n, sizeof n) == 8 && n >= 5 && n <= 14); printf(" (%llu expirations)\n", (unsigned long long)n); /* epoll wakes up when the timer fires. */ int ep = epoll_create1(EPOLL_CLOEXEC); struct epoll_event ev = { .events = EPOLLIN, .data.u64 = 42 }, out; arm(fd, 0, 100, 0); read(fd, &n, sizeof n); /* drop anything left from the interval timer */ epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev); t0 = now_ms(); r = epoll_wait(ep, &out, 1, 3000); took = now_ms() - t0; chk("timerfd wakes epoll_wait", r == 1 && out.data.u64 == 42 && took < 1500); close(ep); /* Rustbelt doesn't do TFD_TIMER_CANCEL_ON_SET: it must say EINVAL. */ struct itimerspec abs = { { 0, 0 }, { rt.tv_sec + 60, 0 } }; errno = 0; r = timerfd_settime(fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &abs, NULL); chk(on_rustbelt ? "TFD_TIMER_CANCEL_ON_SET -> EINVAL (Rustbelt)" : "TFD_TIMER_CANCEL_ON_SET (Linux: works)", on_rustbelt ? r == -1 && errno == EINVAL : r == 0); close(fd); } int main(void) { struct utsname u; setvbuf(stdout, NULL, _IOLBF, 0); /* lines appear in order with the kernel's */ on_rustbelt = uname(&u) == 0 && strcmp(u.sysname, "Rustbelt") == 0; printf("fprobe: Foundry OS libc self-check on %s %s\n", u.sysname, u.release); elf_and_auxv(); files(); processes(); threads_and_memory(); clocks_and_timerfd(); if (fails) printf("fprobe: %d of %d checks FAILED\n", fails, checks); else printf("fprobe: all %d checks passed\n", checks); return fails; }