$RodHat_
MOTD

`getrandom()` skips the kernel now. Took long enough.

Published by

`getrandom()` skips the kernel now. Took long enough.
Photo: AI-generated — no human photographer / RodHat AI Cover

getrandom() shipped in Linux 3.17. That was 2014. A simple interface to the kernel CSPRNG — no /dev/urandom file descriptor, no open/read/close ceremony, just ask the kernel for random bytes and get them. Clean. Better than what it replaced.

And for a decade, every call to it paid a full syscall roundtrip into the kernel. Every TLS handshake generating ephemeral keys. Every UUID. Every session token. Every nonce. Trap into the kernel, copy bytes out, return. Hundreds of nanoseconds per call, minimum — more under virt, more on a machine with Spectre mitigations piling on the context switch cost.

Linux 6.11 fixed this. Jason Donenfeld — the same person who maintains random.c and wrote WireGuard — landed vDSO support for getrandom(). The fast path no longer traps into the kernel at all.

What vDSO actually is

The virtual dynamic shared object is a small shared memory region the kernel maps into every process’s address space at startup. Read-only from userspace, writeable only from the kernel. It’s how clock_gettime() has been blazing fast for years: instead of trapping into the kernel to read the clock, your process reads a per-CPU timestamp from the vDSO page and does the arithmetic in userspace. The kernel updates the vDSO page periodically; your process reads it without a syscall.

The same mechanism, applied to randomness, is subtler — because a clock read is idempotent and a random read is not. You cannot cache random bytes in a shared page and let everyone read them; that’s not random bytes, that’s a broadcast.

Donenfeld’s implementation handles this correctly. The vDSO page carries per-CPU ChaCha20 state: a key, a counter, a generation number. When you call getrandom() and glibc routes you through the vDSO fast path, it grabs the per-CPU state, generates bytes using ChaCha20 in userspace, and increments the counter locally. No syscall. No kernel involvement. The kernel seeds and periodically refreshes the ChaCha20 keys in the vDSO page, but the actual byte generation happens in your process.

The per-CPU design matters: two threads on different cores each have their own state, so there’s no contention even under parallel TLS load. The generation number matters: if the kernel revokes the key — which it does on fork(), VM snapshot restore, and during early boot before the entropy pool is seeded — the generation number changes, the fast path detects a mismatch, and falls back to the real syscall. You get a correct result either way. The fast path is just not taken.

Why this took ten years

clock_gettime() was a natural fit for the vDSO because the data it serves is inherently shared — one timestamp update from the kernel, many reads from userspace. The tricky part was already solved.

getrandom() is harder because the contract is “each call gets unique bytes.” Serving those from shared memory without making them non-unique requires per-CPU state, careful atomic accounting, and a correct invalidation protocol for the cases where userspace state diverges from kernel state (forks, snapshots). The implementation complexity is legitimately higher.

The other piece is that randomness is security-critical. The wrong implementation of this optimization is catastrophically worse than no optimization at all. Get the invalidation protocol wrong after a fork and you’ve handed two processes the same keystream. Get the early-boot seeding check wrong and you’ve handed a process random bytes that aren’t random yet.

Donenfeld got the protocol right. The generation counter approach is clean — it’s essentially the same pattern as seqlock readers detecting a write in progress, adapted for key revocation. glibc 2.39 wired up the vDSO fast path automatically.

I’m not going to pretend the decade gap is fine. It’s not. getrandom() was added in 3.17 specifically to replace the /dev/urandom pattern and fix a class of early-boot race conditions. The syscall overhead was obvious from day one for anyone profiling TLS stacks. The fix existed conceptually — vDSO, per-CPU state — and it took ten years to land because this work requires a person who both understands the security constraints deeply enough not to break them and has the context on random.c to do it correctly. That’s a rare combination. Donenfeld is that person.

What you should do

Check that you’re running a kernel ≥ 6.11 and glibc ≥ 2.39. On those versions, fast getrandom() is automatic. There’s nothing to configure.

To verify the fast path is being taken:

# strace will show getrandom() calls if they fall back to the syscall.
# With the vDSO fast path active, you should see very few or none:
strace -e trace=getrandom -c -- openssl speed rsa2048 2>&1 | head -20

# If you want to count syscall hits in a running process:
# (requires bpftrace, see /tips/2026-08-16-bpftrace-one-liners/)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_getrandom { @[comm] = count(); } interval:s:5 { print(@); clear(@); exit(); }'

If strace shows a flood of getrandom syscalls from a TLS-heavy process on a 6.11+ kernel, check your glibc version first. Then check whether the process is doing something that triggers the fallback path — running inside a container with a seccomp profile that blocks the vDSO bootstrap, or a custom allocator that initializes before glibc’s startup code runs. Those are edge cases; most processes just work.

The TCP internals from ss -i are useful when you want to see the TLS layer’s effects on connection behavior. The entropy story is the layer below that: the keys those connections are using are now generated in userspace, without a kernel trap, and they’re correct.

The verdict

The syscall is still there. The fallback is real and it fires in the cases where it has to. The fast path is an optimization, not a bypass.

For a decade, every call to getrandom() was a minimum of a hundred nanoseconds in a world where modern TLS implementations call it dozens of times per handshake. That cost is gone on 6.11+ with glibc 2.39+. In production, on a busy HTTPS server, this is measurable.

Ten years is too long. The implementation, once it arrived, is correct. I’ll give credit where it’s due: random.c is one of the few subsystems in the kernel where the person doing the work genuinely understands the security model from first principles and isn’t just cargo-culting patterns from adjacent code. That’s why this is the right implementation rather than a fast one that breaks on fork.

Worth the wait. Shouldn’t have taken that long.

Sources

  1. vDSO support for getrandom() — LWN.net
  2. getrandom(2) — Linux manual page — man7.org