foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Edward <edward@localhost> | 2026-10-31 18:35:48 -0700 |
|---|---|---|
| commit | c075970bdb6422c0ba75b72634da2c7710504c53 | |
| parent | 0b513e9df34ecdda27531435d8b774fe5e56bb96 | |
| tree | browse files at this commit | |
Stage 19: vDSO, static PIE and mremap (0.19.0) - vDSO (vdso/vdso.c + vdso.lds, built by build.rs, embedded in the kernel; src/vdso.rs): __vdso_clock_gettime/gettimeofday/time/getcpu (LINUX_2.6), mapped into every process (also after exec) with a read-only vvar page under a seqlock; AT_SYSINFO_EHDR (and AT_HWCAP). REALTIME/MONOTONIC (+_COARSE/_RAW) from the TSC when it's trusted (invariant, or under a hypervisor: QEMU TCG has no invtsc), other clocks and the fallback via syscalls. getcpu via rdtscp (TSC_AUX). New syscalls gettimeofday (96), time (201), getcpu (309). Kernel clock in nanoseconds (clock_getres 1 ns). The vvar page can't be made writable (EACCES). munmap/mprotect accept all of user space. - Static PIE: ET_DYN without PT_INTERP loads at a random page-aligned base; AT_PHDR/AT_ENTRY/AT_BASE(0); PT_GNU_STACK respected, PT_TLS and PT_GNU_RELRO accepted; PT_INTERP -> ENOEXEC; ET_EXEC unchanged. - mremap (25): shrink, grow in place, MREMAP_MAYMOVE (moves PTEs, no copy), MREMAP_FIXED; mmap regions only. - musl-gcc C programs in the build: vdsotest (ET_EXEC at our base), piehello, piethreads, remaptest (static PIE), dynhello (PT_INTERP). - Tests: tests/vdso.rs (7), tests/static_pie.rs (5), tests/mremap.rs (2); fat.rs expects dynhello to be refused; coretest clock_getres 1 ns; run-headless: 44 programs, Linux /dev modes for fb0/hvc0. - README: stage 19 chapter, tables; version 0.19.0.
| M | coretest.rs | +1 -1 |
| A | dynhello.c | +13 -0 |
| M | nbtest.rs | +1 -1 |
| A | piehello.c | +65 -0 |
| A | piethreads.c | +122 -0 |
| A | remaptest.c | +221 -0 |
| A | vdsotest.c | +251 -0 |
7 files changed, 674 insertions, 2 deletions
@@ -447,7 +447,7 @@ fn info(c: &mut Checks) { c.check(ru[0] * 1_000_000 + ru[1] + ru[2] * 1_000_000 + ru[3] >= 100_000, "getrusage(RUSAGE_CHILDREN) has it too"); c.check(syscall3(SYS_GETRUSAGE, 5, ru.as_mut_ptr() as u64, 0) == -EINVAL, "getrusage(5): EINVAL"); let mut res = [0u64; 2];- c.check(syscall3(SYS_CLOCK_GETRES, 1, res.as_mut_ptr() as u64, 0) == 0 && res == [0, 1000], "clock_getres(MONOTONIC): 1 µs");+ c.check(syscall3(SYS_CLOCK_GETRES, 1, res.as_mut_ptr() as u64, 0) == 0 && res == [0, 1], "clock_getres(MONOTONIC): 1 ns (stage 19; was 1 µs)"); c.check(syscall3(SYS_CLOCK_GETRES, 2, res.as_mut_ptr() as u64, 0) == 0 && res == [0, 10_000_000], "clock_getres(PROCESS_CPUTIME): 10 ms"); c.check(syscall3(SYS_CLOCK_GETRES, 0, 0, 0) == 0, "clock_getres with a NULL pointer: 0"); c.check(syscall3(SYS_CLOCK_GETRES, 42, res.as_mut_ptr() as u64, 0) == -EINVAL, "clock_getres(42): EINVAL");
@@ -0,0 +1,13 @@+/*+ * dynhello.c -- stage 19: a *dynamically linked* program (plain+ * `musl-gcc`, no -static). Its ELF file has a PT_INTERP header naming+ * the dynamic loader /lib/ld-musl-x86_64.so.1, which Rustbelt doesn't+ * have, so execve must fail with ENOEXEC ("Exec format error").+ */+#include <stdio.h>++int main(void)+{+ puts("hello from a dynamic program (you shouldn't see this on Rustbelt)");+ return 0;+}
@@ -137,7 +137,7 @@ fn main() -> i64 { c.check(n > 0 && cwd[0] == b'/', "getcwd gives an absolute path"); let mut ub = [0u8; 6 * 65]; let u = uname(&mut ub);- c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 18"), "uname: Rustbelt, stage 18");+ c.check(u[0] == "Rustbelt" && u[3].starts_with("stage 19"), "uname: Rustbelt, stage 19"); let (secs, _) = clock_gettime(0); c.check(secs > 1_600_000_000, "CLOCK_REALTIME is a real date (after 2020)");
@@ -0,0 +1,65 @@+/*+ * piehello.c -- stage 19: "hello" as a static PIE, built with the stock+ * `musl-gcc -static-pie` (no special flags). The file's addresses start+ * at 0; the kernel loads it at a random base and tells us where through+ * the auxiliary vector. musl's start-up code (rcrt1.o) then fixes up the+ * program's own pointers before main runs.+ *+ * Exit code = number of failed checks. `piehello where` prints the load+ * address of main (tests/static_pie.rs runs it twice: different bases).+ */+#define _GNU_SOURCE+#include <elf.h>+#include <link.h>+#include <stdio.h>+#include <string.h>+#include <sys/auxv.h>++static int fails;+#define CHECK(cond, what) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("piehello: FAIL %s\n", what); \+ } \+ } while (0)++/* Pointers in data: these need fixing up (relocating) at start-up. */+static const char *greeting = "hello from a static PIE";+static int (*say)(const char *) = puts;++extern char _start[];+extern const Elf64_Ehdr __ehdr_start; /* the linker's name for our ELF header */++int main(int argc, char **argv)+{+ if (argc > 1 && strcmp(argv[1], "where") == 0) {+ printf("%p\n", (void *)main);+ return 0;+ }+ say(greeting);+ unsigned long phdr = getauxval(AT_PHDR), entry = getauxval(AT_ENTRY);+ CHECK(getauxval(AT_BASE) == 0, "AT_BASE is 0 (no dynamic loader)");+ CHECK(entry == (unsigned long)_start, "AT_ENTRY is where _start is");+ CHECK(getauxval(AT_PHENT) == sizeof(Elf64_Phdr), "AT_PHENT");+ /* Our ELF header is loaded too, at the load bias (the file's lowest+ * address is 0), and the program headers follow it, where AT_PHDR says. */+ unsigned long bias = (unsigned long)&__ehdr_start;+ CHECK(phdr == bias + __ehdr_start.e_phoff, "AT_PHDR points at the program headers");+ const Elf64_Phdr *ph = (const Elf64_Phdr *)phdr;+ int has_tls = 0, has_relro = 0, has_interp = 0;+ for (unsigned long i = 0; i < getauxval(AT_PHNUM); i++) {+ has_tls |= ph[i].p_type == PT_TLS;+ has_relro |= ph[i].p_type == PT_GNU_RELRO;+ has_interp |= ph[i].p_type == PT_INTERP;+ }+ CHECK(bias != 0 && bias % 4096 == 0, "a page-aligned, non-zero load bias");+ CHECK(bias >= 0x8000000000UL, "loaded in Rustbelt's user space");+ CHECK(has_relro && !has_interp, "PT_GNU_RELRO, no PT_INTERP");+ (void)has_tls;+ /* The relocated pointers point into the loaded image. */+ CHECK((unsigned long)greeting > bias, "relocated string pointer");+ /* RELRO: musl made it read-only after relocating; mprotect worked. */+ printf("argv[0] = %s, %d argument(s), bias %#lx\n", argv[0], argc, bias);+ return fails;+}
@@ -0,0 +1,122 @@+/*+ * piethreads.c -- stage 19: a static PIE (stock `musl-gcc -static-pie`)+ * using pthreads, thread-local storage (PT_TLS), mutexes and stdio, plus+ * musl's ttyname() on a pseudo-terminal. Exit code = failed checks.+ */+#define _GNU_SOURCE+#include <errno.h>+#include <fcntl.h>+#include <pthread.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/stat.h>+#include <sys/sysmacros.h>+#include <time.h>+#include <unistd.h>++static int fails;+#define CHECK(cond, ...) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("piethreads: FAIL "); \+ printf(__VA_ARGS__); \+ printf("\n"); \+ } \+ } while (0)++#define THREADS 4+#define ROUNDS 10000++static __thread int mine = 7; /* PT_TLS: initialized ... */+static __thread char scratch[64]; /* ... and zeroed (.tbss) */+static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;+static long shared;+static FILE *out;++static void *worker(void *arg)+{+ long id = (long)arg;+ if (mine != 7 || scratch[0] != 0)+ return (void *)1; /* every thread starts with a fresh TLS copy */+ mine = id;+ snprintf(scratch, sizeof scratch, "thread %ld", id);+ for (int i = 0; i < ROUNDS; i++) {+ pthread_mutex_lock(&lock);+ shared++;+ pthread_mutex_unlock(&lock);+ }+ pthread_mutex_lock(&lock);+ fprintf(out, "%s done\n", scratch);+ pthread_mutex_unlock(&lock);+ /* Nobody else changed our copy meanwhile. */+ return (void *)(long)(mine != id);+}++static void threads(void)+{+ const char *path = "/tmp/piethreads.txt";+ out = fopen(path, "w");+ CHECK(out != NULL, "fopen: errno %d", errno);+ if (!out)+ return;+ pthread_t t[THREADS];+ for (long i = 0; i < THREADS; i++)+ CHECK(pthread_create(&t[i], NULL, worker, (void *)i) == 0, "pthread_create %ld", i);+ for (int i = 0; i < THREADS; i++) {+ void *r;+ pthread_join(t[i], &r);+ CHECK(r == NULL, "thread %d saw wrong TLS", i);+ }+ CHECK(shared == (long)THREADS * ROUNDS, "counter %ld", shared);+ CHECK(mine == 7, "main thread's TLS changed");+ fclose(out);+ /* Read the file back through stdio: one line per thread. */+ FILE *in = fopen(path, "r");+ char line[64];+ int lines = 0;+ while (in && fgets(line, sizeof line, in))+ lines += strstr(line, " done\n") != NULL;+ if (in)+ fclose(in);+ CHECK(lines == THREADS, "%d lines in the file", lines);+ unlink(path);+}++/* musl's ttyname: readlink /proc/self/fd/N, then stat() that name and+ * fstat() the fd -- both must give the same device and inode (stage 19). */+static void tty_names(void)+{+ int master = posix_openpt(O_RDWR | O_NOCTTY);+ CHECK(master >= 0, "posix_openpt: errno %d", errno);+ if (master < 0)+ return;+ CHECK(grantpt(master) == 0 && unlockpt(master) == 0, "grantpt/unlockpt");+ char *name = ptsname(master);+ CHECK(name && strncmp(name, "/dev/pts/", 9) == 0, "ptsname %s", name ? name : "(null)");+ int slave = name ? open(name, O_RDWR | O_NOCTTY) : -1;+ CHECK(slave >= 0, "open %s: errno %d", name, errno);+ if (slave >= 0) {+ char *tn = ttyname(slave);+ CHECK(tn && strcmp(tn, name) == 0, "ttyname(slave) = %s, errno %d", tn ? tn : "(null)", errno);+ struct stat st;+ CHECK(fstat(slave, &st) == 0 && S_ISCHR(st.st_mode) && major(st.st_rdev) == 136, "pts rdev");+ close(slave);+ }+ char *tm = ttyname(master);+ CHECK(tm && strcmp(tm, "/dev/ptmx") == 0, "ttyname(master) = %s", tm ? tm : "(null)");+ close(master);+ if (isatty(0)) {+ char *t0 = ttyname(0);+ CHECK(t0 != NULL, "ttyname(0): errno %d", errno);+ }+}++int main(void)+{+ threads();+ tty_names();+ printf("piethreads: %d failed\n", fails);+ return fails;+}
@@ -0,0 +1,221 @@+/*+ * remaptest.c -- stage 19: mremap, checked from musl.+ *+ * Built with `musl-gcc -static-pie` (see build.rs). mremap makes an mmap+ * region bigger or smaller, moving it if needed; realloc() uses it for big+ * blocks so the bytes don't have to be copied.+ *+ * remaptest all checks (exit code = number of failed checks)+ */+#define _GNU_SOURCE+#include <errno.h>+#include <fcntl.h>+#include <signal.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/mman.h>+#include <sys/wait.h>+#include <unistd.h>++#define PG 4096UL++static int fails;+#define CHECK(cond, ...) \+ do { \+ if (!(cond)) { \+ fails++; \+ printf("remaptest: FAIL line %d: ", __LINE__); \+ printf(__VA_ARGS__); \+ printf("\n"); \+ } \+ } while (0)++static char *anon(size_t pages)+{+ char *p = mmap(0, pages * PG, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);+ return p == MAP_FAILED ? 0 : p;+}++static void fill(char *p, size_t pages, int seed)+{+ for (size_t i = 0; i < pages; i++)+ memset(p + i * PG, seed + (int)i, PG);+}++static int same(const char *p, size_t pages, int seed)+{+ for (size_t i = 0; i < pages; i++)+ for (size_t j = 0; j < PG; j += 512)+ if (p[i * PG + j] != (char)(seed + (int)i))+ return 0;+ return 1;+}++static int zero(const char *p, size_t len)+{+ for (size_t i = 0; i < len; i += 256)+ if (p[i])+ return 0;+ return 1;+}++/* Does touching `p` kill a child with SIGSEGV? (= nothing mapped there) */+static int faults(volatile char *p)+{+ pid_t pid = fork();+ if (pid == 0) {+ (void)*p;+ _exit(0);+ }+ int st = 0;+ waitpid(pid, &st, 0);+ return WIFSIGNALED(st) && WTERMSIG(st) == SIGSEGV;+}++static void grow_in_place(void)+{+ char *p = anon(8);+ CHECK(p, "mmap 8 pages");+ munmap(p + 3 * PG, 5 * PG); /* keep 3, leave room behind them */+ fill(p, 3, 'a');+ char *q = mremap(p, 3 * PG, 6 * PG, 0);+ CHECK(q == p, "grow in place: %p, wanted %p", q, p);+ if (q != p)+ return;+ CHECK(same(p, 3, 'a'), "grow in place kept the data");+ CHECK(zero(p + 3 * PG, 3 * PG), "the new part is zero");+ p[6 * PG - 1] = 1; /* and writable */+ q = mremap(p, 6 * PG, 2 * PG, 0);+ CHECK(q == p, "shrink: %p", q);+ CHECK(same(p, 2, 'a'), "shrink kept the data");+ CHECK(faults(p + 2 * PG), "the cut-off part is gone");+ CHECK(mremap(p, 2 * PG, 2 * PG, 0) == p, "same size: nothing to do");+ munmap(p, 2 * PG);+}++static void move(void)+{+ char *a = anon(2);+ char *b = anon(1); /* right below a: can't grow in place */+ CHECK(a && b && b + PG == a, "b (%p) sits right below a (%p)", b, a);+ fill(b, 1, 'm');+ errno = 0;+ CHECK(mremap(b, PG, 4 * PG, 0) == MAP_FAILED && errno == ENOMEM, "no room and no MAYMOVE: ENOMEM (%d)", errno);+ char *c = mremap(b, PG, 4 * PG, MREMAP_MAYMOVE);+ CHECK(c != MAP_FAILED && c != b, "MAYMOVE moved it: %p", c);+ if (c == MAP_FAILED)+ return;+ CHECK(same(c, 1, 'm'), "the moved page kept its bytes");+ CHECK(zero(c + PG, 3 * PG), "the rest is zero");+ CHECK(faults(b), "nothing left at the old address");++ /* MREMAP_FIXED: to an address we pick. */+ char *spot = anon(4);+ munmap(spot, 4 * PG);+ char *d = mremap(c, 4 * PG, 2 * PG, MREMAP_MAYMOVE | MREMAP_FIXED, spot);+ CHECK(d == spot, "MREMAP_FIXED: %p, wanted %p", d, spot);+ if (d == spot)+ CHECK(same(d, 1, 'm'), "MREMAP_FIXED kept the bytes");+ CHECK(faults(c), "and the old place is empty");+ munmap(spot, 2 * PG);+ munmap(a, 2 * PG);+}++static void errors(void)+{+ char *p = anon(2);+ errno = 0;+ CHECK(mremap(p + 1, PG, 2 * PG, MREMAP_MAYMOVE) == MAP_FAILED && errno == EINVAL, "unaligned: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, PG, 0, MREMAP_MAYMOVE) == MAP_FAILED && errno == EINVAL, "new_len 0: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, PG, 2 * PG, 64) == MAP_FAILED && errno == EINVAL, "unknown flag: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, PG, 2 * PG, MREMAP_FIXED, p + 8 * PG) == MAP_FAILED && errno == EINVAL,+ "FIXED without MAYMOVE: EINVAL (%d)", errno);+ errno = 0;+ CHECK(mremap(p, 2 * PG, 4 * PG, MREMAP_MAYMOVE | MREMAP_FIXED, p + PG) == MAP_FAILED && errno == EINVAL,+ "FIXED onto itself: EINVAL (%d)", errno);+ munmap(p, 2 * PG);+ errno = 0;+ CHECK(mremap(p, PG, 2 * PG, MREMAP_MAYMOVE) == MAP_FAILED && errno == EFAULT, "nothing mapped: EFAULT (%d)", errno);+}++static void file_backed(void)+{+ int fd = open("/tmp/remap.dat", O_RDWR | O_CREAT | O_TRUNC, 0644);+ CHECK(fd >= 0, "open /tmp/remap.dat");+ char page[PG];+ for (int i = 0; i < 3; i++) {+ memset(page, 'F' + i, PG);+ write(fd, page, PG);+ }+ char *keep = anon(1); /* so the mapping below can't grow in place */+ char *f = mmap(0, PG, PROT_READ, MAP_PRIVATE, fd, 0);+ (void)keep;+ CHECK(f != MAP_FAILED && f[0] == 'F', "file page 0");+ char *g = mremap(f, PG, 3 * PG, MREMAP_MAYMOVE);+ CHECK(g != MAP_FAILED, "mremap a file mapping");+ if (g != MAP_FAILED) {+ CHECK(g[0] == 'F' && g[PG] == 'G' && g[2 * PG + 7] == 'H', "moved file pages read the file: %c %c %c", g[0],+ g[PG], g[2 * PG + 7]);+ munmap(g, 3 * PG);+ }+ munmap(keep, PG);+ close(fd);+ unlink("/tmp/remap.dat");+}++static void after_fork(void)+{+ char *p = anon(1);+ fill(p, 1, 'p');+ pid_t pid = fork();+ if (pid == 0) {+ char *q = mremap(p, PG, 16 * PG, MREMAP_MAYMOVE);+ if (q == MAP_FAILED || q[0] != 'p')+ _exit(1);+ q[0] = 'c'; /* copy-on-write must still work after the move */+ _exit(q[0] == 'c' ? 0 : 2);+ }+ int st = 0;+ waitpid(pid, &st, 0);+ CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "child moved and wrote: status %#x", st);+ CHECK(p[0] == 'p', "parent's copy unchanged: %c", p[0]);+ munmap(p, PG);+}++static void with_realloc(void)+{+ size_t n = 1 << 20;+ unsigned *v = malloc(n);+ CHECK(v, "malloc 1 MiB");+ for (size_t i = 0; i < n / 4; i++)+ v[i] = (unsigned)i * 2654435761u;+ for (size_t big = 4 << 20; big <= 32 << 20; big *= 2) {+ v = realloc(v, big);+ CHECK(v, "realloc %zu", big);+ if (!v)+ return;+ size_t bad = 0;+ for (size_t i = 0; i < n / 4; i++)+ bad += v[i] != (unsigned)i * 2654435761u;+ CHECK(bad == 0, "realloc to %zu MiB kept the data (%zu wrong)", big >> 20, bad);+ }+ v = realloc(v, 2 << 20);+ CHECK(v && v[12345] == 12345u * 2654435761u, "realloc down");+ free(v);+}++int main(void)+{+ grow_in_place();+ move();+ errors();+ file_backed();+ after_fork();+ with_realloc();+ printf("remaptest: %s (%d failed)\n", fails ? "FAILED" : "all passed", fails);+ return fails;+}
@@ -0,0 +1,251 @@+/*+ * 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 <elf.h>+#include <errno.h>+#include <sched.h>+#include <signal.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/auxv.h>+#include <sys/mman.h>+#include <sys/syscall.h>+#include <sys/time.h>+#include <sys/wait.h>+#include <time.h>+#include <unistd.h>++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;+}