/* * vdsotest.c -- stage 19: the vDSO, checked from a real C library (musl). * * Built with `musl-gcc -static` as a classic ET_EXEC program (linked at * Rustbelt's user base, see build.rs). musl finds the vDSO through * AT_SYSINFO_EHDR and then calls __vdso_clock_gettime instead of making a * system call. We compare what the vDSO says with what the real system * call says, check the clocks never go backwards, and that it all still * works in a forked child and after execve. * * vdsotest all checks (exit code = number of failed checks) * vdsotest calls just call the clock functions a lot, print nothing * (tests/vdso.rs looks at the --trace output) * vdsotest exec (used internally: the program after execve) */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include static int fails; #define CHECK(cond, ...) \ do { \ if (!(cond)) { \ fails++; \ printf("vdsotest: FAIL line %d: ", __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) extern char _start[]; /* musl's entry point (crt1.o) */ static long long ns(struct timespec t) { return t.tv_sec * 1000000000LL + t.tv_nsec; } static long long us(struct timeval t) { return t.tv_sec * 1000000LL + t.tv_usec; } /* The real system call, bypassing the C library (and so the vDSO). */ static long long sys_ns(int clock) { struct timespec t; if (syscall(SYS_clock_gettime, clock, &t) != 0) return -1; return ns(t); } static long long vdso_ns(int clock) { struct timespec t; if (clock_gettime(clock, &t) != 0) return -1; return ns(t); } /* The image behind AT_SYSINFO_EHDR is an ELF shared object, and the page * in front of it (the vvar page) is readable but can't be made writable. */ static void image(void) { unsigned long base = getauxval(AT_SYSINFO_EHDR); CHECK(base != 0, "no AT_SYSINFO_EHDR"); if (!base) return; const Elf64_Ehdr *eh = (const Elf64_Ehdr *)base; CHECK(memcmp(eh->e_ident, ELFMAG, SELFMAG) == 0, "vDSO has no ELF magic"); CHECK(eh->e_type == ET_DYN, "vDSO e_type %d", eh->e_type); void *vvar = (void *)(base - 4096); unsigned seq = *(volatile unsigned *)vvar; CHECK(seq % 2 == 0, "vvar seq %u is odd", seq); errno = 0; CHECK(mprotect(vvar, 4096, PROT_READ | PROT_WRITE) == -1 && errno == EACCES, "mprotect(vvar, RW): errno %d", errno); /* Other auxv entries a C library looks at. */ CHECK(getauxval(AT_PAGESZ) == 4096, "AT_PAGESZ"); CHECK(getauxval(AT_CLKTCK) == 100, "AT_CLKTCK"); CHECK(getauxval(AT_HWCAP) & (1 << 4), "AT_HWCAP has no TSC bit"); CHECK(getauxval(AT_RANDOM) != 0, "AT_RANDOM"); CHECK(getauxval(AT_SECURE) == 0, "AT_SECURE"); CHECK(getauxval(AT_ENTRY) == (unsigned long)_start, "AT_ENTRY %lx", getauxval(AT_ENTRY)); CHECK(getauxval(AT_PHNUM) > 0 && getauxval(AT_PHENT) == sizeof(Elf64_Phdr), "AT_PHNUM/AT_PHENT"); CHECK(getauxval(AT_BASE) == 0, "AT_BASE (no dynamic loader)"); const char *platform = (const char *)getauxval(AT_PLATFORM); CHECK(platform && strcmp(platform, "x86_64") == 0, "AT_PLATFORM"); const char *execfn = (const char *)getauxval(AT_EXECFN); CHECK(execfn && strstr(execfn, "vdsotest"), "AT_EXECFN"); } /* Each fast clock: vDSO, then syscall, then vDSO again must be in order * (they compute the same formula from the same numbers); and the clocks * never go backwards. */ static void clocks(void) { static const int fast[] = { CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_MONOTONIC_RAW, CLOCK_REALTIME_COARSE, CLOCK_MONOTONIC_COARSE }; for (unsigned i = 0; i < sizeof fast / sizeof fast[0]; i++) { int c = fast[i]; for (int round = 0; round < 100; round++) { long long a = vdso_ns(c), b = sys_ns(c), d = vdso_ns(c); if (!(a > 0 && a <= b && b <= d)) { CHECK(0, "clock %d out of order: vdso %lld, syscall %lld, vdso %lld", c, a, b, d); break; } } } long long prev = vdso_ns(CLOCK_MONOTONIC); for (int i = 0; i < 100000; i++) { long long now = vdso_ns(CLOCK_MONOTONIC); if (now < prev) { CHECK(0, "CLOCK_MONOTONIC went back from %lld to %lld", prev, now); break; } prev = now; } /* Nanoseconds really: two readings in a row usually differ by less * than a microsecond, but they must not be stuck on whole µs. */ int sub_us = 0; for (int i = 0; i < 100 && !sub_us; i++) sub_us = vdso_ns(CLOCK_MONOTONIC) % 1000 != 0; CHECK(sub_us, "CLOCK_MONOTONIC only has microseconds"); /* The wall clock is about now (after 2020), and REALTIME = MONOTONIC + boot. */ CHECK(vdso_ns(CLOCK_REALTIME) / 1000000000 > 1577836800LL, "CLOCK_REALTIME is before 2020"); /* Other clocks take the syscall path inside the vDSO, and still work. */ struct timespec t; CHECK(clock_gettime(CLOCK_BOOTTIME, &t) == 0 && ns(t) > 0, "CLOCK_BOOTTIME"); CHECK(clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t) == 0, "CLOCK_PROCESS_CPUTIME_ID"); CHECK(clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t) == 0, "CLOCK_THREAD_CPUTIME_ID"); errno = 0; CHECK(clock_gettime(12345, &t) == -1 && errno == EINVAL, "bad clock: errno %d", errno); /* gettimeofday and time: also from the vDSO, also agreeing. */ struct timeval tv1, tv2, tvs; struct timezone tz = { 99, 99 }; gettimeofday(&tv1, NULL); CHECK(syscall(SYS_gettimeofday, &tvs, &tz) == 0, "gettimeofday syscall"); gettimeofday(&tv2, NULL); CHECK(us(tv1) <= us(tvs) && us(tvs) <= us(tv2), "gettimeofday out of order: %lld %lld %lld", us(tv1), us(tvs), us(tv2)); CHECK(tz.tz_minuteswest == 0 && tz.tz_dsttime == 0, "timezone not UTC"); CHECK(tv1.tv_usec >= 0 && tv1.tv_usec < 1000000, "tv_usec %ld", (long)tv1.tv_usec); time_t t1 = time(NULL), ts = syscall(SYS_time, NULL), t2; time(&t2); CHECK(t1 <= ts && ts <= t2 && t2 - t1 < 2, "time out of order: %ld %ld %ld", (long)t1, (long)ts, (long)t2); CHECK(t1 == tv1.tv_sec || t1 == tv1.tv_sec + 1, "time %ld vs gettimeofday %ld", (long)t1, (long)tv1.tv_sec); /* sched_getcpu (getcpu in the vDSO): one of our 4 CPUs. */ int cpu = sched_getcpu(); CHECK(cpu >= 0 && cpu < 64, "sched_getcpu %d", cpu); unsigned scpu = 999, node = 999; CHECK(syscall(SYS_getcpu, &scpu, &node, NULL) == 0 && scpu < 64 && node == 0, "getcpu syscall %u %u", scpu, node); /* Pin ourselves? No sched_setaffinity here: just check both agree * at least once in a few tries (the scheduler may move us between). */ int same = 0; for (int i = 0; i < 50 && !same; i++) { cpu = sched_getcpu(); syscall(SYS_getcpu, &scpu, NULL, NULL); same = (unsigned)cpu == scpu; } CHECK(same, "sched_getcpu %d never matched getcpu %u", cpu, scpu); } /* Writing to the vvar page is a SIGSEGV (it's mapped read-only). */ static void vvar_is_read_only(void) { unsigned long base = getauxval(AT_SYSINFO_EHDR); if (!base) return; pid_t pid = fork(); if (pid == 0) { *(volatile unsigned *)(base - 4096) = 1; _exit(0); } int status = 0; waitpid(pid, &status, 0); CHECK(WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV, "write to vvar: status %#x", status); } /* A forked child has the vDSO too (same pages, same answers). */ static void after_fork(void) { long long before = vdso_ns(CLOCK_MONOTONIC); pid_t pid = fork(); if (pid == 0) { long long a = vdso_ns(CLOCK_MONOTONIC), b = sys_ns(CLOCK_MONOTONIC), c = vdso_ns(CLOCK_MONOTONIC); _exit(getauxval(AT_SYSINFO_EHDR) && a >= before && a <= b && b <= c ? 0 : 1); } int status = 0; waitpid(pid, &status, 0); CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0, "vDSO after fork: status %#x", status); } /* And a program started by execve gets a fresh one. */ static void after_exec(const char *self) { pid_t pid = fork(); if (pid == 0) { char *argv[] = { (char *)self, "exec", NULL }; execv(self, argv); _exit(99); } int status = 0; waitpid(pid, &status, 0); CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0, "vdsotest after execve: status %#x", status); } /* A dynamically linked program (PT_INTERP) can't run here: ENOEXEC. */ static void dynamic_is_enoexec(void) { char *argv[] = { "dynhello", NULL }; errno = 0; execv("/mnt/disk0/bin/dynhello", argv); CHECK(errno == ENOEXEC, "execve of a dynamic program: errno %d", errno); } int main(int argc, char **argv) { if (argc > 1 && strcmp(argv[1], "calls") == 0) { /* Lots of clock reads; tests/vdso.rs checks none is a syscall. */ struct timespec t; struct timeval tv; long long sum = 0; for (int i = 0; i < 1000; i++) { clock_gettime(CLOCK_MONOTONIC, &t); sum += t.tv_nsec; clock_gettime(CLOCK_REALTIME, &t); gettimeofday(&tv, NULL); sum += time(NULL) + sched_getcpu(); } return sum == 42; /* use the result; 0 */ } if (argc > 1 && strcmp(argv[1], "exec") == 0) { image(); clocks(); return fails; } image(); clocks(); vvar_is_read_only(); after_fork(); after_exec((const char *)getauxval(AT_EXECFN)); dynamic_is_enoexec(); printf("vdsotest: %d failed\n", fails); return fails; }