getrandom(2) without the syscall: what Linux 6.11 finally shipped
Published by RodHat

Every time your process calls getrandom(2), it crosses the user-kernel boundary, copies bytes from a kernel buffer, and crosses back. For a server generating a session token every few milliseconds, that’s fine. For a TLS implementation calling it hundreds of times per second, or an allocator salting every heap allocation, the syscall overhead adds up. Not catastrophically, but measurably, and entirely avoidably.
OpenBSD has had arc4random(3) in libc since 1997. FreeBSD got it. NetBSD got it. The pattern is simple: seed from the kernel once at startup, then run a userspace ChaCha20 stream cipher for all subsequent calls. No syscall. The kernel is not involved. The only time you need kernel entropy again is after fork (the child gets the same RNG state as the parent, which is a problem) and after exec.
Linux did not do this. Linux shipped getrandom(2) in 3.17 (October 2014), which was good, it fixed the early-boot entropy mess that /dev/urandom never handled correctly, but it was still a syscall. Every call crossed the boundary.
Linux 6.11 (September 2024) finally fixed this.
How the vDSO path works
The vDSO is a small shared library that the kernel maps into every process’s address space. It already handles clock_gettime(2), gettimeofday(2), and getcpu(2) this way: the kernel maintains shared memory that the vDSO reads directly, no syscall needed.
Jason Donenfeld (the WireGuard author) implemented the same mechanism for getrandom. The kernel allocates per-CPU RNG state pages and maps them read-only into the vDSO’s address space. The vDSO getrandom function:
- Locks a per-thread ChaCha20 state (allocated lazily in the thread’s stack via
mmapon first use). - Checks a generation counter the kernel maintains in the shared state. If the kernel has reseeded since the last call, the local state is stale.
- If stale: falls back to the real syscall to pull fresh seed material from the kernel’s CSPRNG. This reseeds the local ChaCha20 state.
- If current: runs ChaCha20 over the local state in userspace and returns the output.
Step 4 is the hot path. No syscall, no kernel involvement. The kernel’s job is to maintain the shared state and bump the generation counter when it reseeds (roughly every few minutes, or after a significant entropy event).
Verifying with strace
On a kernel older than 6.11, every getrandom call shows up:
$ strace -e getrandom ./your-program
getrandom("\xde\xad\xbe\xef...", 32, 0) = 32
getrandom("\xca\xfe\xba\xbe...", 32, 0) = 32
getrandom("\x01\x23\x45\x67...", 32, 0) = 32
On 6.11+, with a libc that uses the vDSO path:
$ strace -e getrandom ./your-program
$
Nothing. The calls still happen logically, but strace only intercepts syscalls. If getrandom runs entirely in userspace, strace sees nothing. You’ll see the occasional syscall when the vDSO state needs reseeding, which is infrequent enough to be invisible in most traces.
If you’re not sure which path your libc takes, ltrace on the vDSO function shows the calls, but that’s harder to set up. The easier test: run perf stat -e syscalls:sys_enter_getrandom ./your-program before and after upgrading. If the counter drops to zero (or near it) on 6.11, you’re on the fast path.
The fork problem
This is where it gets non-obvious. When a process forks, the child inherits a copy of the parent’s memory. If the parent’s vDSO getrandom has local ChaCha20 state, the child has an identical copy of that state. Run getrandom in the parent and child and they’ll produce the same sequence until someone reseeds.
The vDSO implementation handles this with a process-local generation nonce. On the first call after fork in the child, the nonce mismatch triggers a reseed via syscall. You do not have to call anything explicitly. The kernel’s approach: the vDSO stores a counter that is validated against a kernel-visible value that changes on fork, so the first post-fork call detects the fork and falls back to the syscall path to reseed.
This is the same problem that older arc4random(3) implementations got wrong. Early versions on Linux and some BSD ports didn’t have pthread_atfork handlers, so a fork without an explicit arc4random_stir() in the child would produce correlated output. The vDSO getrandom does not have this defect. The kernel-coordinated generation counter makes it automatic.
Early boot and GRND_INSECURE
The vDSO fast path is not available until the kernel’s entropy pool is initialized. On modern Linux (5.4+), the pool initializes from hardware sources (RDRAND, RDSEED, TPM, hwrng drivers) during boot, usually well before any userspace process needs random bytes. But if you’re writing init tooling or very early boot code, the fast path may not be available yet.
In that case, the vDSO falls back to the real getrandom(2) syscall. If you call getrandom without GRND_INSECURE or GRND_NONBLOCK and the pool isn’t ready, the call blocks until it is. That is usually the right behavior. GRND_NONBLOCK returns EAGAIN instead of blocking. GRND_INSECURE (added in 5.6) returns bytes without blocking even if the pool isn’t seeded, and you get a man-page warning that says “you may be getting weak output” in so many words. For most server code, the default (blocking until seeded) is correct.
The GRND_INSECURE flag does pass through to the vDSO path on recent kernels. It gets its own fast path using a different (weaker, or rather: not-yet-entropy-mixed) state. The flag is there for cases where any bytes are better than blocking, not for general use.
What this means in practice
For most code, nothing changes. If you call getrandom(2) (directly or through a libc wrapper like getentropy(3)) and your kernel is 6.11+ and your libc is recent enough to use the vDSO path (glibc 2.41+, musl added support in early 2025), you’re already on the fast path without changing a line.
The places where it matters:
/* A UUID generator called in a tight loop. On 6.11+, these don't syscall. */
for (int i = 0; i < 100000; i++) {
getrandom(uuid_bytes, 16, 0);
/* ... */
}
/* A nonce generator for per-message AEAD. Hot path on TLS stacks. */
getrandom(nonce, 12, 0);
encrypt_and_send(plaintext, len, nonce, key);
The seam to watch is anything that generates random material in a tight loop. Before 6.11, the right optimization was to batch: call getrandom once, pull 256 bytes or so, and chop it up yourself. That’s still valid and avoids even the vDSO overhead (ChaCha20 state setup, lock), but it adds complexity. On 6.11+ with the vDSO path, the overhead of individual calls is low enough that the batching optimization is rarely worth the code.
The BSD scoreboard
FreeBSD has had arc4random(3) doing this without a syscall since approximately forever. The implementation uses ChaCha20 (since 2014, before that it was actual RC4, which was bad), seeds from the kernel at startup and after fork via _getentropy(), and runs entirely in userspace for every subsequent call. On FreeBSD, strace (or truss) on an arc4random-heavy workload shows no entropy-related syscalls outside of startup.
OpenBSD does the same, and they’re the ones who designed the modern arc4random API. When OpenBSD did the RC4-to-ChaCha20 migration in 5.5 (May 2014), they did it quietly. The API didn’t change. Callers didn’t need to know. That’s how a well-designed RNG API should work.
Linux got there in 6.11. Ten years after getrandom, thirty years after arc4random. Better late than never. The implementation is good: the generation-counter approach for detecting fork and reseed events is cleaner than the atfork-handler approach that caused portability headaches in the BSD libraries. Donenfeld’s implementation is worth reading if you want to understand how vDSO-shared kernel state works. The relevant code is in lib/vdso/getrandom.c and crypto/chacha.c in the 6.11 tree.
The io_uring post covers the same zero-syscall theme for I/O: the kernel provides a submission ring that userspace writes to directly, deferring into the kernel in batches rather than per-operation. Different mechanism, same basic argument: a syscall per operation is often not necessary.
The strace post covers how to filter strace output to specific syscalls, which is useful for verifying that getrandom calls disappear on a 6.11+ kernel: strace -e trace=getrandom gives you just the entropy-related syscalls without the rest of the noise.