diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index 153a3f4837e8..33c244cee92d 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -69,3 +69,4 @@ obj-$(CONFIG_TMR_INJECT)	+= xilinx_tmr_inject.o
 obj-$(CONFIG_TPS6594_ESM)	+= tps6594-esm.o
 obj-$(CONFIG_TPS6594_PFSM)	+= tps6594-pfsm.o
 obj-$(CONFIG_NSM)		+= nsm.o
+obj-$(CONFIG_PROCLOCAL_TEST)	+= proclocal-test.o
diff --git a/tools/testing/selftests/proclocal/Makefile b/tools/testing/selftests/proclocal/Makefile
new file mode 100644
index 000000000000..b93baecee762
--- /dev/null
+++ b/tools/testing/selftests/proclocal/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_GEN_PROGS := proclocal-test
+CFLAGS += -O2 -g -Wall $(KHDR_INCLUDES)
+
+include ../lib.mk
diff --git a/drivers/misc/proclocal-test.c b/drivers/misc/proclocal-test.c
new file mode 100644
index 000000000000..9b3d0ed9b2f9
--- /dev/null
+++ b/drivers/misc/proclocal-test.c
@@ -0,0 +1,200 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2024 Amazon.com, Inc. or its affiliates. All rights reserved.
+ * Author: Roman Kagan <rkagan@amazon.de>
+ *
+ * test driver for proclocal memory allocator
+ */
+
+#include <linux/compat.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/workqueue.h>
+#include <linux/file.h>
+#include <linux/secretmem.h>
+
+struct proclocal_test_alloc {
+	u64 size;
+	u64 ptr;
+};
+
+#define PROCLOCAL_TEST_ALLOC _IOWR('A', 0x10, struct proclocal_test_alloc)
+
+#define BOUNCE_BUF_SIZE PAGE_SIZE
+
+struct proclocal_test {
+	struct secretmem_area *area;
+	size_t size;
+	void *bounce;
+};
+
+static int proclocal_test_open(struct inode *inode, struct file *f)
+{
+	struct proclocal_test *plt;
+
+	plt = kzalloc(sizeof(*plt), GFP_KERNEL);
+	if (!plt)
+		return -ENOMEM;
+
+	plt->bounce = kmalloc(BOUNCE_BUF_SIZE, GFP_KERNEL);
+	if (!plt->bounce) {
+		kfree(plt);
+		return -ENOMEM;
+	}
+
+	f->f_mode |= FMODE_UNSIGNED_OFFSET;
+	f->private_data = plt;
+	return 0;
+}
+
+static int proclocal_test_release(struct inode *inode, struct file *f)
+{
+	struct proclocal_test *plt = f->private_data;
+	if (plt->area)
+		secretmem_release_pages(plt->area);
+	kfree(plt->bounce);
+	kfree(plt);
+	return 0;
+}
+
+static ssize_t proclocal_test_read(struct file *f, char __user *buf,
+				   size_t count, loff_t *ppos)
+{
+	struct proclocal_test *plt = f->private_data;
+	const void *p = (const void *)*ppos;
+	ssize_t ret = -EFAULT;
+
+	if (p + count < p)
+		return -EINVAL;
+
+	while (count) {
+		size_t chunk = min_t(size_t, count, BOUNCE_BUF_SIZE);
+		size_t left;
+
+		/*
+		 * copy_to_user() disables superuser checks, so need to copy to
+		 * bounce buffer first to test the access
+		 */
+		memcpy(plt->bounce, p, chunk);
+
+		left = copy_to_user(buf, plt->bounce, chunk);
+		if (left == chunk)
+			goto out;
+		chunk -= left;
+
+		buf += chunk;
+		p += chunk;
+		count -= chunk;
+	}
+
+	ret = p - (const void *)*ppos;
+	*ppos = (loff_t)p;
+out:
+	return ret;
+}
+
+static ssize_t proclocal_test_write(struct file *f, const char __user *buf,
+				    size_t count, loff_t *ppos)
+{
+	struct proclocal_test *plt = f->private_data;
+	void *p = (void *)*ppos;
+	ssize_t ret = -EFAULT;
+
+	if (p + count < p)
+		return -EINVAL;
+
+	while (count) {
+		size_t chunk = min_t(size_t, count, BOUNCE_BUF_SIZE);
+		size_t left;
+
+		/*
+		 * copy_from_user() disables superuser checks, so need to copy
+		 * to bounce buffer first to test the access
+		 */
+		left = copy_from_user(plt->bounce, buf, chunk);
+		if (left == chunk)
+			goto out;
+		chunk -= left;
+
+		memcpy(p, plt->bounce, chunk);
+
+		buf += chunk;
+		p += chunk;
+		count -= chunk;
+	}
+
+	ret = p - (void *)*ppos;
+	*ppos = (loff_t)p;
+out:
+	return ret;
+}
+
+static long proclocal_test_alloc(struct proclocal_test *plt,
+				 void __user *argp)
+{
+	struct proclocal_test_alloc pta;
+	unsigned long pages_needed;
+
+	if (plt->size)
+		return -EEXIST;
+
+	if (copy_from_user(&pta, argp, sizeof(pta)))
+		return -EFAULT;
+
+	if (!pta.size)
+		return -EINVAL;
+
+	pages_needed = (pta.size + PAGE_SIZE - 1) / PAGE_SIZE;
+	plt->area = secretmem_allocate_pages(fls(pages_needed - 1));
+	if (!plt->area)
+		return -ENOMEM;
+
+	plt->size = pta.size;
+
+	pta.ptr = (u64)plt->area->ptr;
+	if (copy_to_user(argp, &pta, sizeof(pta)))
+		goto err;
+
+	return 0;
+err:
+	secretmem_release_pages(plt->area);
+	plt->area = NULL;
+	plt->size = 0;
+	return -EFAULT;
+}
+
+static long proclocal_test_ioctl(struct file *f, unsigned int ioctl,
+				 unsigned long arg)
+{
+	struct proclocal_test *plt = f->private_data;
+	void __user *argp = (void __user *)arg;
+
+	switch (ioctl) {
+	case PROCLOCAL_TEST_ALLOC:
+		return proclocal_test_alloc(plt, argp);
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct file_operations proclocal_test_fops = {
+	.owner          = THIS_MODULE,
+	.release        = proclocal_test_release,
+	.unlocked_ioctl = proclocal_test_ioctl,
+	.compat_ioctl   = compat_ptr_ioctl,
+	.open           = proclocal_test_open,
+	.read           = proclocal_test_read,
+	.write          = proclocal_test_write,
+	.llseek		= no_seek_end_llseek,
+};
+
+static struct miscdevice proclocal_test_misc = {
+	.minor = MISC_DYNAMIC_MINOR,
+	.name  = "proclocal-test",
+	.fops  = &proclocal_test_fops,
+};
+module_misc_device(proclocal_test_misc);
+
+MODULE_VERSION("0.0.1");
+MODULE_LICENSE("GPL v2");
+MODULE_AUTHOR("Roman Kagan");
+MODULE_DESCRIPTION("Test driver for proclocal allocator");
diff --git a/tools/testing/selftests/proclocal/proclocal-test.c b/tools/testing/selftests/proclocal/proclocal-test.c
new file mode 100644
index 000000000000..386cc5d9e51a
--- /dev/null
+++ b/tools/testing/selftests/proclocal/proclocal-test.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (C) 2024 Amazon.com, Inc. or its affiliates. All rights reserved.
+ * Author: Roman Kagan <rkagan@amazon.de>
+ *
+ * test for proclocal memory allocator using the corresponding test device
+ */
+
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include "../kselftest_harness.h"
+
+struct proclocal_test_alloc {
+	uint64_t size;
+	uint64_t ptr;
+};
+
+#define PROCLOCAL_TEST_ALLOC _IOWR('A', 0x10, struct proclocal_test_alloc)
+
+const char proclocal_content[] = "this is test";
+char buf[256];
+
+FIXTURE(proclocal) {
+	int fd;
+	void *ptr;
+};
+
+FIXTURE_SETUP(proclocal)
+{
+	struct proclocal_test_alloc pta = {
+		.size = sizeof(buf),
+	};
+
+	self->fd = open("/dev/proclocal-test", O_RDWR);
+	ASSERT_LE(0, self->fd);
+
+	ASSERT_LE(0, ioctl(self->fd, PROCLOCAL_TEST_ALLOC, &pta));
+
+	self->ptr = (void *) pta.ptr;
+	TH_LOG("self->ptr = %p\n", self->ptr);
+}
+
+FIXTURE_TEARDOWN(proclocal)
+{
+}
+
+TEST_F(proclocal, kernel_access)
+{
+	ASSERT_EQ((off_t)self->ptr,
+		  lseek(self->fd, (off_t)self->ptr, SEEK_SET));
+	EXPECT_EQ(sizeof(proclocal_content),
+		  write(self->fd,
+			proclocal_content, sizeof(proclocal_content)));
+	ASSERT_EQ((off_t)self->ptr,
+		  lseek(self->fd, (off_t)self->ptr, SEEK_SET));
+	EXPECT_EQ(sizeof(proclocal_content),
+		  read(self->fd, buf, sizeof(proclocal_content)));
+	EXPECT_STREQ(proclocal_content, buf);
+}
+
+sigjmp_buf jmpbuf;
+void segv_handler(int signum, siginfo_t *si, void *uc)
+{
+	if (signum == SIGSEGV)
+		siglongjmp(jmpbuf, 1);
+}
+
+TEST_F(proclocal, direct_access)
+{
+	bool access_succeeded;
+	struct sigaction sa;
+
+	if (sigsetjmp(jmpbuf, 1) == 0) {
+		sa.sa_sigaction = segv_handler;
+		sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
+		sigemptyset(&sa.sa_mask);
+
+		sigaction(SIGSEGV, &sa, NULL);
+
+		(void)((volatile char *)self->ptr)[0];
+
+		access_succeeded = true;
+	} else
+		access_succeeded = false;
+
+	EXPECT_FALSE(access_succeeded);
+}
+
+#define PAGE_SIZE 0x1000
+
+TEST_F(proclocal, map_over)
+{
+	void *ptr_page = (void *)((uintptr_t)self->ptr & ~(PAGE_SIZE - 1));
+	void *map;
+	int errno_save;
+
+	errno = 0;
+	map = mmap(ptr_page, PAGE_SIZE, PROT_NONE,
+		   MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
+	errno_save = errno;
+
+	EXPECT_EQ(MAP_FAILED, map);
+	TH_LOG("errno = %d", errno_save);
+
+	if (map != MAP_FAILED)
+		munmap(map, PAGE_SIZE);
+}
+
+TEST_F(proclocal, release)
+{
+	EXPECT_EQ(0, close(self->fd));
+}
+
+TEST_F(proclocal, map_over_closed)
+{
+	void *ptr_page = (void *)((uintptr_t)self->ptr & ~(PAGE_SIZE - 1));
+	void *map;
+	int errno_save;
+
+	ASSERT_EQ(0, close(self->fd));
+
+	errno = 0;
+	map = mmap(ptr_page, PAGE_SIZE, PROT_NONE,
+		   MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
+	errno_save = errno;
+
+	EXPECT_EQ(ptr_page, map);
+	TH_LOG("errno = %d", errno_save);
+
+	if (map != MAP_FAILED)
+		munmap(map, PAGE_SIZE);
+}
+
+TEST_F(proclocal, kernel_access_closed)
+{
+	ASSERT_EQ(0, close(self->fd));
+	self->fd = open("/dev/proclocal-test", O_RDWR);
+	ASSERT_LE(0, self->fd);
+
+	ASSERT_EQ((off_t)self->ptr,
+		  lseek(self->fd, (off_t)self->ptr, SEEK_SET));
+	EXPECT_EQ(-1, read(self->fd, buf, sizeof(proclocal_content)));
+}
+
+TEST_HARNESS_MAIN
diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index faf983680040..29a334de0ca8 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -585,6 +585,21 @@ config NSM
 	  To compile this driver as a module, choose M here.
 	  The module will be called nsm.
 
+config PROCLOCAL_TEST
+	tristate "Proclocal allocator test driver"
+	depends on SECRETMEM
+	help
+	  This driver allows to perform functional and stress tests for
+	  proclocal memory allocator.  It exposes /dev/proclocal-test that
+	  userland test programs can use to create and manipulate proclocal
+	  kernel allocations.
+
+	  To compile this driver as a module, choose M here: the module will be
+	  called proclocal-test.
+
+	  If unsure, say N.
+	  This driver is not meant to be used on production systems.
+
 source "drivers/misc/c2port/Kconfig"
 source "drivers/misc/eeprom/Kconfig"
 source "drivers/misc/cb710/Kconfig"
diff --git a/tools/testing/selftests/proclocal/.gitignore b/tools/testing/selftests/proclocal/.gitignore
new file mode 100644
index 000000000000..47e0fdcd6e3a
--- /dev/null
+++ b/tools/testing/selftests/proclocal/.gitignore
@@ -0,0 +1 @@
+/proclocal-test
