new file mode 100644
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0+
+
+test-getrandom: test-getrandom.c
+ $(CC) $(CPPFLAGS) $(CFLAGS) -I ../../../../usr/include/ -O2 -Wall -Wextra -o $@ $^ -ldl
new file mode 100644
@@ -0,0 +1,307 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (C) 2022 Yann Droneaud. All Rights Reserved.
+ */
+
+#include <dlfcn.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <linux/random.h>
+
+static size_t pagesz;
+static size_t discarded;
+
+typedef ssize_t(*getrandom_fn) (void *, size_t, int);
+
+static bool grnd_timestamp;
+static getrandom_fn getrandom_vDSO;
+
+static ssize_t getrandom_syscall(void *buffer, size_t size, int flags)
+{
+ return syscall(SYS_getrandom, buffer, size, flags);
+}
+
+static ssize_t timestamp(getrandom_fn _getrandom, uint64_t *grnd_ts,
+ size_t size)
+{
+ ssize_t ret;
+
+ ret = _getrandom(grnd_ts, size, GRND_TIMESTAMP);
+ if (ret < 0) {
+ fprintf(stderr,
+ "getrandom(,,GRND_TIMESTAMP) failed: %ld (%s)\n", -ret,
+ strerror((int)-ret));
+ return -1;
+ }
+
+ return ret;
+}
+
+static void fetch(getrandom_fn _getrandom, void *buffer, size_t size)
+{
+ ssize_t ret;
+
+ ret = _getrandom(buffer, size, 0);
+ if (ret < 0) {
+ fprintf(stderr, "getrandom(,,0) failed: %ld (%s)\n", -ret,
+ strerror((int)-ret));
+ exit(EXIT_FAILURE);
+ }
+}
+
+struct rng {
+ uint64_t grnd_ts;
+ size_t availsz; /* available bytes in buffer */
+ size_t buffersz; /* buffer size */
+ uint8_t buffer[];
+};
+
+static struct rng *rng;
+
+static void init_rng(void)
+{
+ int r;
+ ssize_t s;
+ void *p;
+
+ r = getpagesize();
+ if (r == -1) {
+ fprintf(stderr, "getpagesize() failed: %d\n", errno);
+ exit(EXIT_FAILURE);
+ }
+
+ pagesz = (size_t)r;
+
+ p = mmap(NULL, pagesz, PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+ if (p == MAP_FAILED) {
+ fprintf(stderr, "mmap() failed: %d\n", errno);
+ exit(EXIT_FAILURE);
+ }
+
+ r = madvise(p, pagesz, MADV_DONTDUMP | MADV_WIPEONFORK);
+ if (r == -1) {
+ fprintf(stderr, "madvise() failed: %d\n", errno);
+ exit(EXIT_FAILURE);
+ }
+
+ r = mlock(p, pagesz);
+ if (r == -1)
+ fprintf(stderr, "mlock() failed: %d\n", errno);
+
+ rng = p;
+
+ s = timestamp(getrandom_syscall, &rng->grnd_ts, sizeof(rng->grnd_ts));
+ if (s == -1)
+ return;
+
+ printf("getrandom() support GRND_TIMESTAMP\n");
+
+ grnd_timestamp = true;
+}
+
+static void init_vdso(void)
+{
+ void *h;
+ void *p;
+
+ h = dlopen("linux-vdso.so.1", RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD);
+ if (!h) {
+ fprintf(stderr, "failed to open vDSO: %s\n", dlerror());
+ return;
+ }
+
+ p = dlsym(h, "__vdso_getrandom");
+ if (!p) {
+ fprintf(stderr, "getrandom() not found in vDSO: %s\n",
+ dlerror());
+ return;
+ }
+
+ printf("found getrandom() in vDSO at %p\n", p);
+
+ getrandom_vDSO = p;
+}
+
+/*
+ * 1) check timestamp isn't expired
+ * 2) if expired or there's not enough data in buffer
+ * a) if expired, reset buffer size,
+ * b) fetch new random stream
+ * c) check timestamp
+ * d) if expired, reset buffer size, go to b)
+ *
+ */
+static void ensure(getrandom_fn _getrandom, size_t request)
+{
+ ssize_t r;
+
+ r = timestamp(_getrandom, &rng->grnd_ts, sizeof(rng->grnd_ts));
+ switch (r) {
+ case 0: /* timestamp didn't change */
+ /* enough available random bytes ? */
+ if (rng->availsz >= request)
+ return;
+
+ /* increase buffer size when drained */
+ if (rng->buffersz < pagesz - sizeof(*rng))
+ rng->buffersz *= 2;
+
+ /* no less than 32 */
+ if (rng->buffersz < 32)
+ rng->buffersz = 32;
+
+ /* no more than a full page minus the rng structure */
+ if (rng->buffersz > pagesz - sizeof(*rng))
+ rng->buffersz = pagesz - sizeof(*rng);
+
+ break;
+
+ case sizeof(rng->grnd_ts): /* timestamp did change, random bytes must be discarded */
+ rng->buffersz = 32; /* reset size */
+ break;
+
+ default:
+ fprintf(stderr, "unexpected timestamp size %zd\n", r);
+ exit(EXIT_FAILURE);
+ }
+
+ /* keep fetching if timestamp is updated */
+ for (;;) {
+ if (rng->availsz)
+ discarded += rng->availsz;
+
+ fetch(_getrandom, rng->buffer, rng->buffersz);
+ rng->availsz = rng->buffersz;
+
+ r = timestamp(_getrandom, &rng->grnd_ts, sizeof(rng->grnd_ts));
+
+ switch (r) {
+ case 0: /* timestamp didn't change between previous check and last fetch */
+ return;
+
+ case sizeof(rng->grnd_ts): /* timestamp did change, random bytes just fetched must be discarded */
+ rng->buffersz = 32; /* reset size */
+ continue; /* retry again */
+
+ default:
+ fprintf(stderr, "unexpected timestamp size %zd\n", r);
+ exit(EXIT_FAILURE);
+ }
+ }
+}
+
+/* arc4random() */
+static void get_direct(getrandom_fn _getrandom)
+{
+ uint32_t v;
+ fetch(_getrandom, &v, sizeof(v));
+}
+
+static void get_pooled(getrandom_fn _getrandom)
+{
+ ensure(_getrandom, sizeof(uint32_t));
+ rng->availsz -= sizeof(uint32_t);
+}
+
+static inline struct timespec timespec_sub(const struct timespec *a,
+ const struct timespec *b)
+{
+ struct timespec res;
+
+ res.tv_sec = a->tv_sec - b->tv_sec;
+ res.tv_nsec = a->tv_nsec - b->tv_nsec;
+ if (res.tv_nsec < 0) {
+ res.tv_sec--;
+ res.tv_nsec += 1000000000L;
+ }
+
+ return res;
+}
+
+#define SAMPLES 13
+#define VALUES (16 * 1024 * 1024)
+
+static void test_direct(getrandom_fn _getrandom, const char *method)
+{
+ struct timespec start, end, diff;
+
+ for (int i = 0; i < SAMPLES; i++) {
+ clock_gettime(CLOCK_MONOTONIC, &start);
+
+ for (uint32_t j = 0; j < VALUES; j++)
+ get_direct(_getrandom);
+
+ clock_gettime(CLOCK_MONOTONIC, &end);
+
+ diff = timespec_sub(&end, &start);
+
+ printf("== direct %s getrandom(), %u u32, %lu.%09lu s, %.3f M u32/s, %.3f ns/u32\n",
+ method, VALUES, diff.tv_sec, diff.tv_nsec,
+ VALUES / (1000000 *
+ (diff.tv_sec +
+ (double)diff.tv_nsec / 1000000000UL)),
+ (double)(diff.tv_sec * 1000000000UL +
+ diff.tv_nsec) / VALUES);
+ }
+}
+
+static void test_pooled(getrandom_fn _getrandom, const char *method)
+{
+ struct timespec start, end, diff;
+
+ for (int i = 0; i < SAMPLES; i++) {
+ discarded = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &start);
+
+ for (uint32_t j = 0; j < VALUES; j++)
+ get_pooled(_getrandom);
+
+ clock_gettime(CLOCK_MONOTONIC, &end);
+
+ diff = timespec_sub(&end, &start);
+
+ printf("== pooled %s getrandom(), %u u32, %lu.%09lu s, %.3f M u32/s, %.3f ns/u32, (%zu bytes discarded)\n",
+ method, VALUES, diff.tv_sec, diff.tv_nsec,
+ VALUES / (1000000 *
+ (diff.tv_sec +
+ (double)diff.tv_nsec / 1000000000UL)),
+ (double)(diff.tv_sec * 1000000000UL +
+ diff.tv_nsec) / VALUES,
+ discarded);
+ }
+}
+
+int main(void)
+{
+ printf("getrandom(,,GRND_TIMESTAMP) test\n");
+
+ init_rng();
+ init_vdso();
+
+ while (1) {
+ test_direct(getrandom_syscall, "syscall");
+
+ if (getrandom_vDSO)
+ test_direct(getrandom_vDSO, "vDSO");
+
+ if (grnd_timestamp)
+ test_pooled(getrandom_syscall, "syscall");
+
+ if (getrandom_vDSO && grnd_timestamp)
+ test_pooled(getrandom_vDSO, "vDSO");
+ }
+
+ return 0;
+}