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

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
- vDSO support for getrandom() — LWN.net
- getrandom(2) — Linux manual page — man7.org