diff --git a/arch/Kconfig b/arch/Kconfig
index 8df1c7102643..a427f6440cc9 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -31,6 +31,7 @@ config HOTPLUG_SMT
 	bool
 
 config GENERIC_ENTRY
+       select UACCESS_BUFFER
        bool
 
 config KPROBES
@@ -1288,6 +1289,11 @@ config ARCH_HAS_ELFCORE_COMPAT
 config ARCH_HAS_PARANOID_L1D_FLUSH
 	bool
 
+config UACCESS_BUFFER
+	bool
+	help
+	  Select if the architecture's syscall entry/exit code supports uaccess buffers.
+
 source "kernel/gcov/Kconfig"
 
 source "scripts/gcc-plugins/Kconfig"
diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig
index 5c7ae4c3954b..4764e5fd7ba9 100644
--- a/arch/arm64/Kconfig
+++ b/arch/arm64/Kconfig
@@ -221,6 +221,7 @@ config ARM64
 	select THREAD_INFO_IN_TASK
 	select HAVE_ARCH_USERFAULTFD_MINOR if USERFAULTFD
 	select TRACE_IRQFLAGS_SUPPORT
+	select UACCESS_BUFFER
 	help
 	  ARM 64-bit (AArch64) Linux support.
 
diff --git a/arch/arm64/kernel/syscall.c b/arch/arm64/kernel/syscall.c
index 50a0f1a38e84..c3f8652d84a5 100644
--- a/arch/arm64/kernel/syscall.c
+++ b/arch/arm64/kernel/syscall.c
@@ -139,7 +139,9 @@ static void el0_svc_common(struct pt_regs *regs, int scno, int sc_nr,
 			goto trace_exit;
 	}
 
+	uaccess_buffer_syscall_entry();
 	invoke_syscall(regs, scno, sc_nr, syscall_table);
+	uaccess_buffer_syscall_exit();
 
 	/*
 	 * The tracing status may have changed under our feet, so we have to
diff --git a/include/linux/instrumented.h b/include/linux/instrumented.h
index 42faebbaa202..9144936edcb1 100644
--- a/include/linux/instrumented.h
+++ b/include/linux/instrumented.h
@@ -2,7 +2,7 @@
 
 /*
  * This header provides generic wrappers for memory access instrumentation that
- * the compiler cannot emit for: KASAN, KCSAN.
+ * the compiler cannot emit for: KASAN, KCSAN, access buffers.
  */
 #ifndef _LINUX_INSTRUMENTED_H
 #define _LINUX_INSTRUMENTED_H
@@ -11,6 +11,7 @@
 #include <linux/kasan-checks.h>
 #include <linux/kcsan-checks.h>
 #include <linux/types.h>
+#include <linux/uaccess_buffer.h>
 
 /**
  * instrument_read - instrument regular read access
@@ -117,6 +118,7 @@ instrument_copy_to_user(void __user *to, const void *from, unsigned long n)
 {
 	kasan_check_read(from, n);
 	kcsan_check_read(from, n);
+	uaccess_buffer_log_write(to, n);
 }
 
 /**
@@ -134,6 +136,7 @@ instrument_copy_from_user(const void *to, const void __user *from, unsigned long
 {
 	kasan_check_write(to, n);
 	kcsan_check_write(to, n);
+	uaccess_buffer_log_read(from, n);
 }
 
 #endif /* _LINUX_INSTRUMENTED_H */
diff --git a/include/linux/sched.h b/include/linux/sched.h
index e12b524426b0..3fecb0487b97 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -34,6 +34,7 @@
 #include <linux/rseq.h>
 #include <linux/seqlock.h>
 #include <linux/kcsan.h>
+#include <linux/uaccess_buffer_info.h>
 #include <asm/kmap_size.h>
 
 /* task_struct member predeclarations (sorted alphabetically): */
@@ -1487,6 +1488,8 @@ struct task_struct {
 	struct callback_head		l1d_flush_kill;
 #endif
 
+	struct uaccess_buffer_info	uaccess_buffer;
+
 	/*
 	 * New fields for task_struct should be added above here, so that
 	 * they are included in the randomized portion of task_struct.
diff --git a/include/linux/uaccess_buffer.h b/include/linux/uaccess_buffer.h
new file mode 100644
index 000000000000..3b81f2a192a4
--- /dev/null
+++ b/include/linux/uaccess_buffer.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_ACCESS_BUFFER_H
+#define _LINUX_ACCESS_BUFFER_H
+
+#include <asm-generic/errno-base.h>
+
+#ifdef CONFIG_UACCESS_BUFFER
+
+void uaccess_buffer_log_read(const void __user *from, unsigned long n);
+void uaccess_buffer_log_write(void __user *to, unsigned long n);
+
+void uaccess_buffer_syscall_entry(void);
+void uaccess_buffer_syscall_exit(void);
+
+int uaccess_buffer_set_logging(unsigned long addr, unsigned long size,
+			       unsigned long store_end_addr);
+
+#else
+
+static inline void uaccess_buffer_log_read(const void __user *from,
+					   unsigned long n)
+{
+}
+static inline void uaccess_buffer_log_write(void __user *to, unsigned long n)
+{
+}
+
+static inline void uaccess_buffer_syscall_entry(void)
+{
+}
+static inline void uaccess_buffer_syscall_exit(void)
+{
+}
+
+static inline int uaccess_buffer_set_logging(unsigned long addr,
+					     unsigned long size,
+					     unsigned long store_end_addr)
+{
+	return -EINVAL;
+}
+#endif
+
+#endif  /* _LINUX_ACCESS_BUFFER_H */
diff --git a/include/linux/uaccess_buffer_info.h b/include/linux/uaccess_buffer_info.h
new file mode 100644
index 000000000000..a6cefe6e73b5
--- /dev/null
+++ b/include/linux/uaccess_buffer_info.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _LINUX_ACCESS_BUFFER_INFO_H
+#define _LINUX_ACCESS_BUFFER_INFO_H
+
+#include <uapi/asm/signal.h>
+
+#ifdef CONFIG_UACCESS_BUFFER
+
+struct uaccess_buffer_info {
+	unsigned long addr, size;
+	unsigned long store_end_addr;
+	sigset_t saved_sigmask;
+	u8 state;
+};
+
+#else
+
+struct uaccess_buffer_info {
+};
+
+#endif
+
+#endif  /* _LINUX_ACCESS_BUFFER_INFO_H */
diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
index 43bd7f713c39..d8baacaef800 100644
--- a/include/uapi/linux/prctl.h
+++ b/include/uapi/linux/prctl.h
@@ -269,4 +269,13 @@ struct prctl_mm_map {
 # define PR_SCHED_CORE_SHARE_FROM	3 /* pull core_sched cookie to pid */
 # define PR_SCHED_CORE_MAX		4
 
+/* Log uaccesses to a user-provided buffer */
+#define PR_LOG_UACCESS			63
+
+/* Format of the entries in the uaccess log. */
+struct uaccess_buffer_entry {
+	__u64 addr, size, flags;
+};
+# define UACCESS_BUFFER_FLAG_WRITE	1 /* access was a write */
+
 #endif /* _LINUX_PRCTL_H */
diff --git a/kernel/Makefile b/kernel/Makefile
index 4df609be42d0..75a5d95ce9c3 100644
--- a/kernel/Makefile
+++ b/kernel/Makefile
@@ -115,6 +115,7 @@ obj-$(CONFIG_KCSAN) += kcsan/
 obj-$(CONFIG_SHADOW_CALL_STACK) += scs.o
 obj-$(CONFIG_HAVE_STATIC_CALL_INLINE) += static_call.o
 obj-$(CONFIG_CFI_CLANG) += cfi.o
+obj-$(CONFIG_UACCESS_BUFFER) += uaccess_buffer.o
 
 obj-$(CONFIG_PERF_EVENTS) += events/
 
diff --git a/kernel/entry/common.c b/kernel/entry/common.c
index bf16395b9e13..c7e7ff8cbab3 100644
--- a/kernel/entry/common.c
+++ b/kernel/entry/common.c
@@ -89,6 +89,8 @@ __syscall_enter_from_user_work(struct pt_regs *regs, long syscall)
 	if (work & SYSCALL_WORK_ENTER)
 		syscall = syscall_trace_enter(regs, syscall, work);
 
+	uaccess_buffer_syscall_entry();
+
 	return syscall;
 }
 
@@ -273,6 +275,7 @@ static void syscall_exit_to_user_mode_prepare(struct pt_regs *regs)
 			local_irq_enable();
 	}
 
+	uaccess_buffer_syscall_exit();
 	rseq_syscall(regs);
 
 	/*
diff --git a/kernel/sys.c b/kernel/sys.c
index 8fdac0d90504..df487600773c 100644
--- a/kernel/sys.c
+++ b/kernel/sys.c
@@ -42,6 +42,7 @@
 #include <linux/version.h>
 #include <linux/ctype.h>
 #include <linux/syscall_user_dispatch.h>
+#include <linux/uaccess_buffer.h>
 
 #include <linux/compat.h>
 #include <linux/syscalls.h>
@@ -2530,6 +2531,11 @@ SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
 		error = sched_core_share_pid(arg2, arg3, arg4, arg5);
 		break;
 #endif
+	case PR_LOG_UACCESS:
+		if (arg5)
+			return -EINVAL;
+		error = uaccess_buffer_set_logging(arg2, arg3, arg4);
+		break;
 	default:
 		error = -EINVAL;
 		break;
diff --git a/kernel/uaccess_buffer.c b/kernel/uaccess_buffer.c
new file mode 100644
index 000000000000..b9da89887c4b
--- /dev/null
+++ b/kernel/uaccess_buffer.c
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/compat.h>
+#include <linux/prctl.h>
+#include <linux/sched.h>
+#include <linux/signal.h>
+#include <linux/uaccess.h>
+#include <linux/uaccess_buffer.h>
+#include <linux/uaccess_buffer_info.h>
+
+#ifdef CONFIG_UACCESS_BUFFER
+
+/*
+ * We use a separate implementation of copy_to_user() that avoids the call
+ * to instrument_copy_to_user() as this would otherwise lead to infinite
+ * recursion.
+ */
+static unsigned long
+uaccess_buffer_copy_to_user(void __user *to, const void *from, unsigned long n)
+{
+	if (!access_ok(to, n))
+		return n;
+	return raw_copy_to_user(to, from, n);
+}
+
+static void uaccess_buffer_log(unsigned long addr, unsigned long size,
+			      unsigned long flags)
+{
+	struct uaccess_buffer_entry entry;
+
+	if (current->uaccess_buffer.size < sizeof(entry) ||
+	    unlikely(uaccess_kernel()))
+		return;
+	entry.addr = addr;
+	entry.size = size;
+	entry.flags = flags;
+
+	/*
+	 * If our uaccess fails, abort the log so that the end address writeback
+	 * does not occur and userspace sees zero accesses.
+	 */
+	if (uaccess_buffer_copy_to_user(
+		    (void __user *)current->uaccess_buffer.addr, &entry,
+		    sizeof(entry))) {
+		current->uaccess_buffer.state = 0;
+		current->uaccess_buffer.addr = current->uaccess_buffer.size = 0;
+	}
+
+	current->uaccess_buffer.addr += sizeof(entry);
+	current->uaccess_buffer.size -= sizeof(entry);
+}
+
+void uaccess_buffer_log_read(const void __user *from, unsigned long n)
+{
+	uaccess_buffer_log((unsigned long)from, n, 0);
+}
+EXPORT_SYMBOL(uaccess_buffer_log_read);
+
+void uaccess_buffer_log_write(void __user *to, unsigned long n)
+{
+	uaccess_buffer_log((unsigned long)to, n, UACCESS_BUFFER_FLAG_WRITE);
+}
+EXPORT_SYMBOL(uaccess_buffer_log_write);
+
+int uaccess_buffer_set_logging(unsigned long addr, unsigned long size,
+			       unsigned long store_end_addr)
+{
+	sigset_t temp_sigmask;
+
+	current->uaccess_buffer.addr = addr;
+	current->uaccess_buffer.size = size;
+	current->uaccess_buffer.store_end_addr = store_end_addr;
+
+	/*
+	 * Allow 2 syscalls before resetting the state: the current one (i.e.
+	 * prctl) and the next one, whose accesses we want to log.
+	 */
+	current->uaccess_buffer.state = 2;
+
+	/*
+	 * Temporarily mask signals so that an intervening asynchronous signal
+	 * will not interfere with the logging.
+	 */
+	current->uaccess_buffer.saved_sigmask = current->blocked;
+	sigfillset(&temp_sigmask);
+	sigdelsetmask(&temp_sigmask, sigmask(SIGKILL) | sigmask(SIGSTOP));
+	__set_current_blocked(&temp_sigmask);
+
+	return 0;
+}
+
+void uaccess_buffer_syscall_entry(void)
+{
+	/*
+	 * The current syscall may be e.g. rt_sigprocmask, and therefore we want
+	 * to reset the mask before the syscall and not after, so that our
+	 * temporary mask is unobservable.
+	 */
+	if (current->uaccess_buffer.state == 1)
+		__set_current_blocked(&current->uaccess_buffer.saved_sigmask);
+}
+
+void uaccess_buffer_syscall_exit(void)
+{
+	if (current->uaccess_buffer.state > 0) {
+		--current->uaccess_buffer.state;
+		if (current->uaccess_buffer.state == 0) {
+			u64 addr64 = current->uaccess_buffer.addr;
+
+			uaccess_buffer_copy_to_user(
+				(void __user *)
+					current->uaccess_buffer.store_end_addr,
+				&addr64, sizeof(addr64));
+			current->uaccess_buffer.addr = current->uaccess_buffer.size = 0;
+		}
+	}
+}
+
+#endif
