diff --git a/arch/x86/entry/vdso/Makefile b/arch/x86/entry/vdso/Makefile
index 76cd790ed0bd..a60d4771d500 100644
--- a/arch/x86/entry/vdso/Makefile
+++ b/arch/x86/entry/vdso/Makefile
@@ -24,7 +24,7 @@ VDSO32-$(CONFIG_X86_32)		:= y
 VDSO32-$(CONFIG_IA32_EMULATION)	:= y
 
 # files to link into the vdso
-vobjs-y := vdso-note.o vclock_gettime.o vgetcpu.o
+vobjs-y := vdso-note.o vclock_gettime.o vgetcpu.o vgetrandom.o
 vobjs32-y := vdso32/note.o vdso32/system_call.o vdso32/sigreturn.o
 vobjs32-y += vdso32/vclock_gettime.o
 vobjs-$(CONFIG_X86_SGX)	+= vsgx.o
diff --git a/arch/x86/entry/vdso/vdso.lds.S b/arch/x86/entry/vdso/vdso.lds.S
index 4bf48462fca7..1919cc39277e 100644
--- a/arch/x86/entry/vdso/vdso.lds.S
+++ b/arch/x86/entry/vdso/vdso.lds.S
@@ -28,6 +28,8 @@ VERSION {
 		clock_getres;
 		__vdso_clock_getres;
 		__vdso_sgx_enter_enclave;
+		getrandom;
+		__vdso_getrandom;
 	local: *;
 	};
 }
diff --git a/arch/x86/entry/vdso/vgetrandom.c b/arch/x86/entry/vdso/vgetrandom.c
new file mode 100644
index 000000000000..40389c399c6a
--- /dev/null
+++ b/arch/x86/entry/vdso/vgetrandom.c
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+#include <linux/kernel.h>
+#include <linux/types.h>
+
+#include "../../../../lib/vdso/getrandom.c"
+
+ssize_t __vdso_getrandom(void **state, void *buffer, size_t len, unsigned long flags)
+{
+	return __cvdso_getrandom(state, buffer, len, flags);
+}
+
+ssize_t getrandom(void **, void *, size_t, unsigned long)
+	__attribute__((weak, alias("__vdso_getrandom")));
diff --git a/arch/x86/include/asm/vdso/getrandom.h b/arch/x86/include/asm/vdso/getrandom.h
new file mode 100644
index 000000000000..83cb483aab74
--- /dev/null
+++ b/arch/x86/include/asm/vdso/getrandom.h
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+#ifndef __ASM_VDSO_GETRANDOM_H
+#define __ASM_VDSO_GETRANDOM_H
+
+#ifndef __ASSEMBLY__
+
+#include <asm/unistd.h>
+#include <asm/vvar.h>
+
+static __always_inline ssize_t
+getrandom_syscall(void *buffer, size_t len, unsigned long flags)
+{
+	long ret;
+
+	asm ("syscall" : "=a" (ret) :
+	     "0" (__NR_getrandom), "D" (buffer), "S" (len), "d" (flags) :
+	     "rcx", "r11", "memory");
+
+	return ret;
+}
+
+static __always_inline void *
+mmap_syscall(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
+{
+	long ret;
+	register long r10 asm("r10") = flags;
+	register long r8 asm("r8") = fd;
+	register long r9 asm("r9") = offset;
+
+	asm ("syscall" : "=a" (ret) :
+	     "0" (__NR_mmap), "D" (addr), "S" (len), "d" (prot),
+	     "r" (r10), "r" (r8), "r" (r9) :
+	     "rcx", "r11");
+
+	return (void *)ret;
+}
+
+static __always_inline int
+munmap_syscall(void *addr, size_t len)
+{
+	long ret;
+
+	asm ("syscall" : "=a" (ret) :
+	     "0" (__NR_munmap), "D" (addr), "S" (len) :
+	     "rcx", "r11");
+
+	return ret;
+}
+
+static __always_inline int
+madvise_syscall(void *addr, size_t len, int advice)
+{
+	long ret;
+
+	asm ("syscall" : "=a" (ret) :
+	     "0" (__NR_madvise), "D" (addr), "S" (len), "d" (advice) :
+	     "rcx", "r11");
+
+	return ret;
+}
+
+#define __vdso_rng_data (VVAR(_vdso_rng_data))
+
+static __always_inline const struct vdso_rng_data *__arch_get_vdso_rng_data(void)
+{
+	return &__vdso_rng_data;
+}
+
+#endif /* !__ASSEMBLY__ */
+
+#endif /* __ASM_VDSO_GETRANDOM_H */
diff --git a/arch/x86/include/asm/vvar.h b/arch/x86/include/asm/vvar.h
index 183e98e49ab9..9d9af37f7cab 100644
--- a/arch/x86/include/asm/vvar.h
+++ b/arch/x86/include/asm/vvar.h
@@ -26,6 +26,8 @@
  */
 #define DECLARE_VVAR(offset, type, name) \
 	EMIT_VVAR(name, offset)
+#define DECLARE_VVAR_SINGLE(offset, type, name) \
+	EMIT_VVAR(name, offset)
 
 #else
 
@@ -37,6 +39,10 @@ extern char __vvar_page;
 	extern type timens_ ## name[CS_BASES]				\
 	__attribute__((visibility("hidden")));				\
 
+#define DECLARE_VVAR_SINGLE(offset, type, name)				\
+	extern type vvar_ ## name					\
+	__attribute__((visibility("hidden")));				\
+
 #define VVAR(name) (vvar_ ## name)
 #define TIMENS(name) (timens_ ## name)
 
@@ -44,12 +50,22 @@ extern char __vvar_page;
 	type name[CS_BASES]						\
 	__attribute__((section(".vvar_" #name), aligned(16))) __visible
 
+#define DEFINE_VVAR_SINGLE(type, name)					\
+	type name							\
+	__attribute__((section(".vvar_" #name), aligned(16))) __visible
+
 #endif
 
 /* DECLARE_VVAR(offset, type, name) */
 
 DECLARE_VVAR(128, struct vdso_data, _vdso_data)
 
+#if !defined(_SINGLE_DATA)
+#define _SINGLE_DATA
+DECLARE_VVAR_SINGLE(640, struct vdso_rng_data, _vdso_rng_data)
+#endif
+
 #undef DECLARE_VVAR
+#undef DECLARE_VVAR_SINGLE
 
 #endif
diff --git a/drivers/char/random.c b/drivers/char/random.c
index 7bf11fa66265..a18ff41713d7 100644
--- a/drivers/char/random.c
+++ b/drivers/char/random.c
@@ -59,6 +59,7 @@
 #include <asm/irq.h>
 #include <asm/irq_regs.h>
 #include <asm/io.h>
+#include <vdso/datapage.h>
 
 /*********************************************************************
  *
@@ -84,6 +85,7 @@ static DEFINE_STATIC_KEY_FALSE(crng_is_ready);
 /* Various types of waiters for crng_init->CRNG_READY transition. */
 static DECLARE_WAIT_QUEUE_HEAD(crng_init_wait);
 static struct fasync_struct *fasync;
+DEFINE_VVAR_SINGLE(struct vdso_rng_data, _vdso_rng_data);
 
 /* Control how we warn userspace. */
 static struct ratelimit_state urandom_warning =
@@ -221,6 +223,7 @@ static void crng_reseed(void)
 		++next_gen;
 	WRITE_ONCE(base_crng.generation, next_gen);
 	WRITE_ONCE(base_crng.birth, jiffies);
+	WRITE_ONCE(_vdso_rng_data.generation, next_gen + 1);
 	if (!static_branch_likely(&crng_is_ready))
 		crng_init = CRNG_READY;
 	spin_unlock_irqrestore(&base_crng.lock, flags);
@@ -660,6 +663,7 @@ static void __cold _credit_init_bits(size_t bits)
 		crng_reseed(); /* Sets crng_init to CRNG_READY under base_crng.lock. */
 		if (static_key_initialized)
 			execute_in_process_context(crng_set_ready, &set_ready);
+		_vdso_rng_data.is_ready = true;
 		wake_up_interruptible(&crng_init_wait);
 		kill_fasync(&fasync, SIGIO, POLL_IN);
 		pr_notice("crng init done\n");
diff --git a/include/vdso/datapage.h b/include/vdso/datapage.h
index 73eb622e7663..cbacfd923a5c 100644
--- a/include/vdso/datapage.h
+++ b/include/vdso/datapage.h
@@ -109,6 +109,11 @@ struct vdso_data {
 	struct arch_vdso_data	arch_data;
 };
 
+struct vdso_rng_data {
+	unsigned long generation;
+	bool is_ready;
+};
+
 /*
  * We use the hidden visibility to prevent the compiler from generating a GOT
  * relocation. Not only is going through a GOT useless (the entry couldn't and
@@ -120,6 +125,7 @@ struct vdso_data {
  */
 extern struct vdso_data _vdso_data[CS_BASES] __attribute__((visibility("hidden")));
 extern struct vdso_data _timens_data[CS_BASES] __attribute__((visibility("hidden")));
+extern struct vdso_rng_data _vdso_rng_data __attribute__((visibility("hidden")));
 
 /*
  * The generic vDSO implementation requires that gettimeofday.h
diff --git a/lib/vdso/getrandom.c b/lib/vdso/getrandom.c
new file mode 100644
index 000000000000..3ffc900f31ff
--- /dev/null
+++ b/lib/vdso/getrandom.c
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2022 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <linux/kernel.h>
+#include <linux/atomic.h>
+#include <linux/fs.h>
+#include <vdso/datapage.h>
+#include <asm/vdso/getrandom.h>
+#include <asm/vdso/vsyscall.h>
+#include <asm/page.h>
+#include <uapi/linux/mman.h>
+#include "../crypto/chacha.c"
+
+#define NEG_NULL ((void *)~0UL)
+
+struct getrandom_state {
+	unsigned long generation;
+	union {
+		struct {
+			u8 key[CHACHA_KEY_SIZE];
+			u8 batch[CHACHA_BLOCK_SIZE * 3 / 2];
+		};
+		u8 key_batch[CHACHA_BLOCK_SIZE * 2];
+	};
+	u8 pos;
+	bool in_use;
+};
+
+enum { NUM_BUCKETS = PAGE_SIZE / sizeof(struct getrandom_state) };
+
+static bool allocate_new_state(void **state)
+{
+	void *new_state;
+
+	if (cmpxchg(state, NULL, NEG_NULL) != NULL)
+		return false;
+
+	new_state = mmap_syscall(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
+				 MAP_PRIVATE | MAP_ANONYMOUS | MAP_LOCKED, -1, 0);
+	if (new_state == NEG_NULL)
+		goto err_unlock;
+
+	if (madvise_syscall(new_state, PAGE_SIZE, MADV_WIPEONFORK))
+		goto err_unmap;
+
+	WRITE_ONCE(*state, new_state);
+	return true;
+
+err_unmap:
+	munmap_syscall(new_state, PAGE_SIZE);
+err_unlock:
+	WRITE_ONCE(*state, NULL);
+	return false;
+}
+
+extern long getcpu(unsigned int *cpu, unsigned int *node, void *unused);
+
+static struct getrandom_state *find_free_bucket(struct getrandom_state *buckets)
+{
+	unsigned int start = 0, i;
+
+	if (getcpu(&start, NULL, NULL) == 0)
+		start %= NUM_BUCKETS;
+
+	for (i = start;;) {
+		struct getrandom_state *state = &buckets[i];
+
+		if (cmpxchg(&state->in_use, false, true) == false)
+			return state;
+
+		i = i == NUM_BUCKETS - 1 ? 0 : i + 1;
+		if (i == start)
+			break;
+	}
+
+	return NULL;
+}
+
+static void memcpy_and_zero(void *dst, void *src, size_t len)
+{
+#define CASCADE(type) \
+	while (len >= sizeof(type)) { \
+		*(type *)dst = *(type *)src; \
+		*(type *)src = 0; \
+		dst += sizeof(type); \
+		src += sizeof(type); \
+		len -= sizeof(type); \
+	}
+#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
+#if BITS_PER_LONG == 64
+	CASCADE(u64);
+#endif
+	CASCADE(u32);
+	CASCADE(u16);
+#endif
+	CASCADE(u8);
+#undef CASCADE
+}
+
+static ssize_t __always_inline
+__cvdso_getrandom(void **state, void *buffer, size_t len, unsigned long flags)
+{
+	const struct vdso_rng_data *v = __arch_get_vdso_rng_data();
+	struct getrandom_state *buckets = *state, *s;
+	u32 chacha_state[CHACHA_STATE_WORDS];
+	unsigned long generation;
+	ssize_t ret = min_t(size_t, MAX_RW_COUNT, len);
+	size_t batch_len;
+
+	if (unlikely(!v->is_ready))
+		return getrandom_syscall(buffer, len, flags);
+
+	if (unlikely(!len))
+		return 0;
+
+	if (unlikely(!buckets)) {
+		if (!allocate_new_state(state))
+			return getrandom_syscall(buffer, len, flags);
+		buckets = *state;
+	} else if (unlikely(buckets == NEG_NULL)) {
+		return getrandom_syscall(buffer, len, flags);
+	}
+
+	s = find_free_bucket(buckets);
+	if (unlikely(!s))
+		return getrandom_syscall(buffer, len, flags);
+
+retry_generation:
+	generation = READ_ONCE(v->generation);
+	if (unlikely(s->generation != generation)) {
+		if (getrandom_syscall(s->key, sizeof(s->key), 0) != sizeof(s->key)) {
+			WRITE_ONCE(s->in_use, false);
+			return getrandom_syscall(buffer, len, flags);
+		}
+		s->pos = sizeof(s->batch);
+		s->generation = generation;
+		if (generation != READ_ONCE(v->generation))
+			goto retry_generation;
+	}
+
+	len = ret;
+more_batch:
+	batch_len = min_t(size_t, sizeof(s->batch) - s->pos, len);
+	if (batch_len) {
+		memcpy_and_zero(buffer, s->batch, batch_len);
+		s->pos += batch_len;
+		buffer += batch_len;
+		len -= batch_len;
+		if (!len) {
+			WRITE_ONCE(s->in_use, false);
+			return ret;
+		}
+	}
+
+	chacha_init_consts(chacha_state);
+	memcpy(&chacha_state[4], s->key, CHACHA_KEY_SIZE);
+	memset(&chacha_state[12], 0, sizeof(u32) * 4);
+
+	while (len >= CHACHA_BLOCK_SIZE) {
+		chacha20_block(chacha_state, buffer);
+		if (unlikely(chacha_state[12] == 0))
+			++chacha_state[13];
+		buffer += CHACHA_BLOCK_SIZE;
+		len -= CHACHA_BLOCK_SIZE;
+	}
+
+	chacha20_block(chacha_state, s->key_batch);
+	if (unlikely(chacha_state[12] == 0))
+		++chacha_state[13];
+	chacha20_block(chacha_state, s->key_batch + CHACHA_BLOCK_SIZE);
+	s->pos = 0;
+	memzero_explicit(chacha_state, sizeof(chacha_state));
+	goto more_batch;
+}
