RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorEdward <edward@localhost>2026-10-31 18:35:48 -0700
commitc075970bdb6422c0ba75b72634da2c7710504c53
parent0b513e9df34ecdda27531435d8b774fe5e56bb96
treebrowse 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.
Mcoretest.rs+1 -1
Adynhello.c+13 -0
Mnbtest.rs+1 -1
Apiehello.c+65 -0
Apiethreads.c+122 -0
Aremaptest.c+221 -0
Avdsotest.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;+}