diff --git a/Documentation/dev-tools/kunit/api/index.rst b/Documentation/dev-tools/kunit/api/index.rst
index 5cdb552a0808f25baeff5e47a9227b7b62c69e40..34d8fee9a97059d6da919a6fb1a7e359b5e0beef 100644
--- a/Documentation/dev-tools/kunit/api/index.rst
+++ b/Documentation/dev-tools/kunit/api/index.rst
@@ -9,6 +9,7 @@ API Reference
 	test
 	resource
 	functionredirection
+	uapi
 	clk
 	of
 	platformdevice
@@ -32,6 +33,10 @@ Documentation/dev-tools/kunit/api/functionredirection.rst
 
  - Documents the KUnit Function Redirection API
 
+Documentation/dev-tools/kunit/api/uapi.rst
+
+ - Documents the KUnit Userspace testing API
+
 Driver KUnit API
 ================
 
diff --git a/Documentation/dev-tools/kunit/api/uapi.rst b/Documentation/dev-tools/kunit/api/uapi.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b4764424c629bf69194cf2786f52aef154b02bf8
--- /dev/null
+++ b/Documentation/dev-tools/kunit/api/uapi.rst
@@ -0,0 +1,12 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+==================
+Userspace Test API
+==================
+
+This file documents all of the userspace testing API.
+Userspace tests should be built as :ref:`userprogs <kbuild_userprogs>` and included into the test
+module or kernel as :ref:`blobs <kbuild_blobs>`.
+
+.. kernel-doc:: include/kunit/uapi.h
+   :internal:
diff --git a/include/kunit/uapi.h b/include/kunit/uapi.h
new file mode 100644
index 0000000000000000000000000000000000000000..a6181790c96a42df05839097991c1fbfd889cdbe
--- /dev/null
+++ b/include/kunit/uapi.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * KUnit Userspace testing API.
+ *
+ * Copyright (C) 2025, Linutronix GmbH.
+ * Author: Thomas Weißschuh <thomas.weissschuh@linutronix.de>
+ */
+
+#ifndef _KUNIT_UAPI_H
+#define _KUNIT_UAPI_H
+
+struct blob;
+struct kunit;
+
+/**
+ * kunit_uapi_run_kselftest() - Run a userspace kselftest as part of kunit
+ * @test: The test context object.
+ * @executable: kselftest executable to run
+ *
+ * Runs the kselftest and forwards its TAP output and exit status to kunit.
+ */
+void kunit_uapi_run_kselftest(struct kunit *test, const struct blob *executable);
+
+#endif /* _KUNIT_UAPI_H */
diff --git a/lib/kunit/Kconfig b/lib/kunit/Kconfig
index a97897edd9642f3e5df7fdd9dee26ee5cf00d6a4..79fcf0565f60100920b9613199421bf9cae1c2b0 100644
--- a/lib/kunit/Kconfig
+++ b/lib/kunit/Kconfig
@@ -93,4 +93,15 @@ config KUNIT_AUTORUN_ENABLED
 	  In most cases this should be left as Y. Only if additional opt-in
 	  behavior is needed should this be set to N.
 
+config KUNIT_UAPI
+	def_bool y
+	depends on KUNIT=y
+	depends on CC_CAN_LINK_STATIC || ARCH_HAS_NOLIBC
+	depends on TMPFS
+	select HEADERS_INSTALL
+	help
+	  Enables support for building and running userspace selftests as part of kunit.
+	  These tests should be statically linked and use kselftest.h or kselftest_harness.h
+	  for status reporting.
+
 endif # KUNIT
diff --git a/lib/kunit/Makefile b/lib/kunit/Makefile
index 5aa51978e456ab3bb60c12071a26cf2bdcb1b508..989933dab9ad2267f376db470b876ce2a88711b4 100644
--- a/lib/kunit/Makefile
+++ b/lib/kunit/Makefile
@@ -12,6 +12,8 @@ kunit-objs +=				test.o \
 					device.o \
 					platform.o
 
+kunit-$(CONFIG_KUNIT_UAPI) +=		uapi.o
+
 ifeq ($(CONFIG_KUNIT_DEBUGFS),y)
 kunit-objs +=				debugfs.o
 endif
diff --git a/lib/kunit/uapi.c b/lib/kunit/uapi.c
new file mode 100644
index 0000000000000000000000000000000000000000..081ddc46e3ccb9ef88f9cb2a66d5e2bf41da2eb4
--- /dev/null
+++ b/lib/kunit/uapi.c
@@ -0,0 +1,287 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit Userspace testing API.
+ *
+ * Copyright (C) 2025, Linutronix GmbH.
+ * Author: Thomas Weißschuh <thomas.weissschuh@linutronix.de>
+ */
+
+#include <linux/binfmts.h>
+#include <linux/blob.h>
+#include <linux/file.h>
+#include <linux/fs.h>
+#include <linux/fs_struct.h>
+#include <linux/pid.h>
+#include <linux/pipe_fs_i.h>
+#include <linux/sched/task.h>
+#include <linux/types.h>
+
+#include <kunit/test.h>
+#include <kunit/uapi.h>
+
+#define KSFT_PASS	0
+#define KSFT_FAIL	1
+#define KSFT_XFAIL	2
+#define KSFT_XPASS	3
+#define KSFT_SKIP	4
+
+static struct vfsmount *kunit_uapi_mount_tmpfs(void)
+{
+	struct file_system_type *type;
+	struct vfsmount *mnt;
+
+	type = get_fs_type("tmpfs");
+	if (!type)
+		return ERR_PTR(-ENODEV);
+
+	/* FIXME
+	 * The mount setup is supposed to look like this:
+	 * kunit_uapi_mount_tmpfs() sets up a private mount,
+	 * with nothing visible except the new tmpfs.
+	 * Then each executable execution gets a new namespace on top of that
+	 * on which it can mount whatever it needs.
+	 * However I didn't manage to set this up, so keep everything simple
+	 * for now and let somebody familiar with the VFS figure this out.
+	 */
+
+	mnt = kern_mount(type);
+	put_filesystem(type);
+
+	return mnt;
+}
+
+static int kunit_uapi_write_file(struct vfsmount *mnt, const char *name, mode_t mode,
+				 const u8 *data, size_t size)
+{
+	struct file *file;
+	ssize_t written;
+
+	file = file_open_root_mnt(mnt, name, O_CREAT | O_WRONLY, mode);
+	if (IS_ERR(file))
+		return PTR_ERR(file);
+
+	written = kernel_write(file, data, size, NULL);
+	filp_close(file, NULL);
+	if (written != size) {
+		if (written >= 0)
+			return -ENOMEM;
+		return written;
+	}
+
+	/* Flush delayed fput so exec can open the file read-only */
+	flush_delayed_fput();
+
+	return 0;
+}
+
+struct kunit_uapi_user_mode_thread_ctx {
+	const char *executable;
+
+	/* Signals mnt, out, pwd and tgid */
+	struct completion setup_done;
+	struct vfsmount *mnt;
+	struct file *out;
+	struct path pwd;
+	pid_t tgid;
+
+	/* Valid after wait(tgid) */
+	int exec_err;
+};
+
+static int kunit_uapi_user_mode_thread_init(void *data)
+{
+	struct kunit_uapi_user_mode_thread_ctx *ctx = data;
+	const char *const argv[] = {
+		ctx->executable,
+		NULL
+	};
+	struct file *out[2];
+	int err;
+
+	err = create_pipe_files(out, 0);
+	if (err)
+		return err;
+
+	/* stdin, use the *write* end to the pipe to have an unreadable input */
+	err = replace_fd(0, out[1], 0);
+	if (err < 0) {
+		fput(out[0]);
+		fput(out[1]);
+		return err;
+	}
+
+	/* stdout */
+	err = replace_fd(1, out[1], 0);
+	if (err < 0) {
+		replace_fd(0, NULL, 0);
+		fput(out[0]);
+		fput(out[1]);
+		return err;
+	}
+
+	/* stderr */
+	err = replace_fd(2, out[1], 0);
+	if (err < 0) {
+		replace_fd(0, NULL, 0);
+		replace_fd(1, NULL, 0);
+		fput(out[0]);
+		fput(out[1]);
+		return err;
+	}
+
+	fput(out[1]);
+
+	ctx->out = out[0];
+	ctx->tgid = current->tgid;
+
+	set_fs_pwd(current->fs, &ctx->pwd);
+	kernel_sigaction(SIGKILL, SIG_DFL);
+	kernel_sigaction(SIGABRT, SIG_DFL);
+
+	complete(&ctx->setup_done);
+	ctx->exec_err = kernel_execve(ctx->executable, argv, NULL);
+	if (!ctx->exec_err)
+		return 0;
+	do_exit(0);
+}
+
+static size_t kunit_uapi_printk_subtest_lines(struct kunit *test, char *buf, size_t s)
+{
+	const char *ptr = buf, *newline;
+	size_t n;
+
+	while (s) {
+		newline = strnchr(ptr, s, '\n');
+		if (!newline)
+			break;
+
+		n = newline - ptr + 1;
+
+		kunit_log(KERN_INFO, test, KUNIT_SUBSUBTEST_INDENT "%.*s", (int)n, ptr);
+		ptr += n;
+		s -= n;
+	}
+
+	memmove(buf, ptr, s);
+
+	return s;
+}
+
+static int kunit_uapi_forward_to_printk(struct kunit *test, struct file *output)
+{
+	/*
+	 * printk() automatically adds a newline after each message.
+	 * Therefore only fully accumulated lines can be forwarded.
+	 * Each line needs to fit into the buffer below.
+	 */
+	char buf[512];
+	size_t s = 0;
+	ssize_t n;
+
+	while (1) {
+		n = kernel_read(output, buf + s, sizeof(buf) - s, NULL);
+		if (n <= 0)
+			return n;
+		s = kunit_uapi_printk_subtest_lines(test, buf, s + n);
+	}
+}
+
+static void kunit_uapi_kill_pid(pid_t pid)
+{
+	struct pid *p;
+
+	p = find_get_pid(pid);
+	kill_pid(p, SIGKILL, 1);
+	put_pid(p);
+}
+
+static int kunit_uapi_run_executable_in_mount(struct kunit *test, const char *executable,
+						   struct vfsmount *mnt)
+{
+	struct kunit_uapi_user_mode_thread_ctx ctx = {
+		.setup_done	= COMPLETION_INITIALIZER_ONSTACK(ctx.setup_done),
+		.executable	= executable,
+		.pwd		= {
+			.mnt	= mnt,
+			.dentry	= mnt->mnt_root,
+		},
+	};
+	int forward_err, wait_err, ret;
+	pid_t pid;
+
+	/* If SIGCHLD is ignored do_wait won't populate the status. */
+	kernel_sigaction(SIGCHLD, SIG_DFL);
+	pid = user_mode_thread(kunit_uapi_user_mode_thread_init, &ctx, SIGCHLD);
+	if (pid < 0) {
+		kernel_sigaction(SIGCHLD, SIG_IGN);
+		return pid;
+	}
+
+	wait_for_completion(&ctx.setup_done);
+
+	forward_err = kunit_uapi_forward_to_printk(test, ctx.out);
+	if (forward_err)
+		kunit_uapi_kill_pid(ctx.tgid);
+
+	wait_err = kernel_wait(ctx.tgid, &ret);
+
+	/* Restore default kernel sig handler */
+	kernel_sigaction(SIGCHLD, SIG_IGN);
+
+	if (ctx.exec_err)
+		return ctx.exec_err;
+	if (forward_err)
+		return forward_err;
+	if (wait_err < 0)
+		return wait_err;
+	return ret;
+}
+
+static int kunit_uapi_run_executable(struct kunit *test, const struct blob *executable)
+{
+	const char *exe_name = kbasename(executable->path);
+	struct vfsmount *mnt;
+	int err;
+
+	mnt = kunit_uapi_mount_tmpfs();
+	if (IS_ERR(mnt))
+		return PTR_ERR(mnt);
+
+	err = kunit_uapi_write_file(mnt, exe_name, 0755, executable->data, blob_size(executable));
+
+	if (!err)
+		err = kunit_uapi_run_executable_in_mount(test, exe_name, mnt);
+
+	kern_unmount(mnt);
+
+	return err;
+}
+
+void kunit_uapi_run_kselftest(struct kunit *test, const struct blob *executable)
+{
+	u8 exit_code, exit_signal;
+	int err;
+
+	err = kunit_uapi_run_executable(test, executable);
+	if (err < 0)
+		KUNIT_FAIL(test, "Could not run test executable: %pe\n", ERR_PTR(err));
+
+	exit_code = err >> 8;
+	exit_signal = err & 0xff;
+
+	if (exit_signal)
+		KUNIT_FAIL(test, "kselftest exited with signal: %d\n", exit_signal);
+	else if (exit_code == KSFT_PASS)
+		; /* Noop */
+	else if (exit_code == KSFT_FAIL)
+		KUNIT_FAIL(test, "kselftest exited with code KSFT_FAIL\n");
+	else if (exit_code == KSFT_XPASS)
+		KUNIT_FAIL(test, "kselftest exited with code KSFT_XPASS\n");
+	else if (exit_code == KSFT_XFAIL)
+		; /* Noop */
+	else if (exit_code == KSFT_SKIP)
+		kunit_mark_skipped(test, "kselftest exited with code KSFT_SKIP\n");
+	else
+		KUNIT_FAIL(test, "kselftest exited with unknown exit code: %d\n", exit_code);
+}
+EXPORT_SYMBOL_GPL(kunit_uapi_run_kselftest);
