Linux Kernel Selftest development
 help / color / mirror / Atom feed
* [PATCH] selftests/keys: Add persistent keyring expiry regression test
@ 2026-10-06 17:39 Sahaj Chaudhari
  2026-10-08 14:57 ` Jarkko Sakkinen
  0 siblings, 1 reply; 2+ messages in thread
From: Sahaj Chaudhari @ 2026-10-06 17:39 UTC (permalink / raw)
  To: shuah; +Cc: linux-kselftest, linux-kernel, dhowells, jarkko, keyrings

KEYCTL_GET_PERSISTENT creates and registers a persistent keyring before
linking it into the caller's destination keyring. If the destination is
restricted, the link returns -EPERM. Verify that the keyring's configured
expiry is still applied after the failed link.

Register the test in kselftest and document direct and QEMU/GDB execution.
The failure case is intended for a disposable VM: an unexpired persistent
keyring may remain registered until its user namespace is torn down.

Tested:

  make -C tools/testing/selftests/keys

  QEMU guest with fixed kernel: TAP pass 1/1

Signed-off-by: Sahaj Chaudhari <sahaj123.sc@gmail.com>
---
 tools/testing/selftests/Makefile              |   1 +
 tools/testing/selftests/keys/.gitignore       |   2 +
 tools/testing/selftests/keys/Makefile         |   6 +
 tools/testing/selftests/keys/README           |  50 +++
 .../testing/selftests/keys/initramfs-init.sh  |  10 +
 .../keys/persistent_keyring_expiry.c          | 367 ++++++++++++++++++
 tools/testing/selftests/keys/run_qemu.sh      |  64 +++
 7 files changed, 500 insertions(+)
 create mode 100644 tools/testing/selftests/keys/.gitignore
 create mode 100644 tools/testing/selftests/keys/Makefile
 create mode 100644 tools/testing/selftests/keys/README
 create mode 100755 tools/testing/selftests/keys/initramfs-init.sh
 create mode 100644 tools/testing/selftests/keys/persistent_keyring_expiry.c
 create mode 100755 tools/testing/selftests/keys/run_qemu.sh

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 273853937c25..d74ad9370bd1 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -62,6 +62,7 @@ TARGETS += ipc
 TARGETS += ir
 TARGETS += kcmp
 TARGETS += kexec
+TARGETS += keys
 TARGETS += kselftest_harness
 TARGETS += kvm
 TARGETS += landlock
diff --git a/tools/testing/selftests/keys/.gitignore b/tools/testing/selftests/keys/.gitignore
new file mode 100644
index 000000000000..48c2900d780e
--- /dev/null
+++ b/tools/testing/selftests/keys/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+persistent_keyring_expiry
diff --git a/tools/testing/selftests/keys/Makefile b/tools/testing/selftests/keys/Makefile
new file mode 100644
index 000000000000..33984d1eceb3
--- /dev/null
+++ b/tools/testing/selftests/keys/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+CFLAGS += -g -Wall $(KHDR_INCLUDES)
+
+TEST_GEN_PROGS := persistent_keyring_expiry
+
+include ../lib.mk
diff --git a/tools/testing/selftests/keys/README b/tools/testing/selftests/keys/README
new file mode 100644
index 000000000000..dd92abc1ddfe
--- /dev/null
+++ b/tools/testing/selftests/keys/README
@@ -0,0 +1,50 @@
+The persistent_keyring_expiry selftest verifies that a failed link from
+KEYCTL_GET_PERSISTENT still gives a newly created persistent keyring its
+configured expiry. It requires CONFIG_KEYS, CONFIG_PERSISTENT_KEYRINGS,
+CONFIG_PROC_FS, procfs, and root privileges. The test temporarily changes
+/proc/sys/kernel/keys/persistent_keyring_expiry and restores its original value.
+
+Build and run it against a kernel containing the fix with:
+
+    make -C tools/testing/selftests/keys
+    sudo tools/testing/selftests/keys/persistent_keyring_expiry
+
+The QEMU/GDB instructions assume a configured Linux source tree with an
+existing `.config` and the standard kernel build toolchain. The QEMU runner
+requires `cpio`, `qemu-system-x86_64`, `busybox`, and `ldd`; GDB is required
+for the interactive debugging steps.
+
+For a QEMU/GDB run, build an x86 kernel with debug symbols and
+CONFIG_PERSISTENT_KEYRINGS=y. Starting from an existing `.config`, enable
+the required options and build `bzImage` and `vmlinux`:
+
+    scripts/config --enable KEYS --enable PERSISTENT_KEYRINGS \
+        --enable PROC_FS --enable DEVTMPFS --enable DEVTMPFS_MOUNT \
+        --disable DEBUG_INFO_NONE --enable DEBUG_INFO \
+        --enable DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT \
+        --enable GDB_SCRIPTS --enable FRAME_POINTER
+    make olddefconfig
+    make -j2 bzImage
+
+Then run:
+
+    tools/testing/selftests/keys/run_qemu.sh path/to/bzImage path/to/vmlinux
+
+In another terminal, attach GDB and continue the paused guest:
+
+    gdb path/to/vmlinux
+    (gdb) target remote localhost:1234
+    (gdb) break key_get_persistent
+    (gdb) break key_set_timeout
+    (gdb) continue
+
+At `key_get_persistent`, inspect `uid` with `info args`. Continue until
+`key_set_timeout` is hit, check `timeout` with `print timeout` (300 seconds),
+then continue to let the test finish. The result is printed on the QEMU serial
+console.
+
+To reproduce the bug, run this test against a kernel built from the parent of
+commit 25bf14f81716. It reports a permanent ("perm") expiry. With the fix, the
+restricted link returns -EPERM and the new keyring has a finite expiry.
+The pre-fix bug can leave a permanent keyring in the test's user namespace
+until that namespace is torn down, so run this reproduction in a disposable VM.
diff --git a/tools/testing/selftests/keys/initramfs-init.sh b/tools/testing/selftests/keys/initramfs-init.sh
new file mode 100755
index 000000000000..47cbfcb68d64
--- /dev/null
+++ b/tools/testing/selftests/keys/initramfs-init.sh
@@ -0,0 +1,10 @@
+#!/bin/busybox sh
+# SPDX-License-Identifier: GPL-2.0
+# shellcheck shell=dash
+
+/bin/busybox mount -t proc procfs /proc
+/keys/persistent_keyring_expiry
+status=$?
+/bin/busybox echo "keyring expiry selftest exit status: $status"
+/bin/busybox poweroff -f
+exit "$status"
diff --git a/tools/testing/selftests/keys/persistent_keyring_expiry.c b/tools/testing/selftests/keys/persistent_keyring_expiry.c
new file mode 100644
index 000000000000..55dbd8ce74ec
--- /dev/null
+++ b/tools/testing/selftests/keys/persistent_keyring_expiry.c
@@ -0,0 +1,367 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/keyctl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "../kselftest.h"
+
+#define EXPIRY_PATH "/proc/sys/kernel/keys/persistent_keyring_expiry"
+#define TEST_EXPIRY 300
+
+static int read_expiry(unsigned int *expiry)
+{
+	char buf[32];
+	char *end;
+	unsigned long value;
+	ssize_t len;
+	int fd;
+
+	fd = open(EXPIRY_PATH, O_RDONLY);
+	if (fd < 0)
+		return -1;
+
+	len = read(fd, buf, sizeof(buf) - 1);
+	close(fd);
+	if (len <= 0)
+		return -1;
+
+	buf[len] = '\0';
+	errno = 0;
+	value = strtoul(buf, &end, 10);
+	if (errno || end == buf || (*end != '\n' && *end != '\0') ||
+	    value > UINT_MAX) {
+		errno = EINVAL;
+		return -1;
+	}
+
+	*expiry = value;
+	return 0;
+}
+
+static int write_expiry(unsigned int expiry)
+{
+	char buf[32];
+	size_t len;
+	ssize_t written;
+	int fd;
+
+	len = snprintf(buf, sizeof(buf), "%u\n", expiry);
+	fd = open(EXPIRY_PATH, O_WRONLY);
+	if (fd < 0)
+		return -1;
+
+	written = write(fd, buf, len);
+	if (written != len) {
+		int saved_errno = errno;
+
+		close(fd);
+		errno = written >= 0 ? EIO : saved_errno;
+		return -1;
+	}
+	if (close(fd) < 0)
+		return -1;
+
+	return 0;
+}
+
+static long add_keyring(const char *description)
+{
+	return syscall(SYS_add_key, "keyring", description, NULL, 0,
+		       KEY_SPEC_SESSION_KEYRING);
+}
+
+static int unlink_key(int key, int keyring)
+{
+	return syscall(SYS_keyctl, KEYCTL_UNLINK, key, keyring, 0, 0);
+}
+
+static int find_persistent_expiry(uid_t uid, char *expiry, size_t expiry_len)
+{
+	char description[32];
+	char *line = NULL;
+	size_t line_len = 0;
+	ssize_t len;
+	FILE *keys;
+	int found = 0;
+
+	snprintf(description, sizeof(description), "_persistent.%u", uid);
+	keys = fopen("/proc/keys", "r");
+	if (!keys)
+		return -1;
+
+	while ((len = getline(&line, &line_len, keys)) >= 0) {
+		char *fields[9];
+		char *saveptr;
+		char *field;
+		unsigned int n = 0;
+		size_t description_len = strlen(description);
+
+		for (field = strtok_r(line, " \t\n", &saveptr);
+		     field && n < ARRAY_SIZE(fields);
+		     field = strtok_r(NULL, " \t\n", &saveptr))
+			fields[n++] = field;
+
+		if (n == ARRAY_SIZE(fields) &&
+		    strncmp(fields[8], description, description_len) == 0 &&
+		    (fields[8][description_len] == '\0' ||
+		     fields[8][description_len] == ':')) {
+			snprintf(expiry, expiry_len, "%s", fields[3]);
+			found = 1;
+			break;
+		}
+	}
+
+	free(line);
+	fclose(keys);
+	return found;
+}
+
+static void dump_persistent_keys(void)
+{
+	char *line = NULL;
+	size_t line_len = 0;
+	FILE *keys = fopen("/proc/keys", "r");
+
+	if (!keys)
+		return;
+
+	while (getline(&line, &line_len, keys) >= 0) {
+		if (strstr(line, "_persistent."))
+			ksft_print_msg("visible key: %s", line);
+	}
+
+	free(line);
+	fclose(keys);
+}
+
+static void set_failure(char *reason, size_t reason_len, const char *fmt, ...)
+{
+	va_list args;
+
+	va_start(args, fmt);
+	vsnprintf(reason, reason_len, fmt, args);
+	va_end(args);
+}
+
+static void report_skip(const char *reason)
+{
+	ksft_test_result_skip("%s\n", reason);
+	ksft_finished();
+}
+
+int main(void)
+{
+	char expiry[32] = {};
+	char reason[256] = {};
+	unsigned int saved_expiry;
+	uid_t test_uid = getuid();
+	int destination = -1;
+	int cleanup_destination = -1;
+	int linked_persistent = -1;
+	int cleanup_persistent = -1;
+	long ret;
+	bool restore_expiry = false;
+	bool passed = false;
+	bool skipped = false;
+	int get_persistent_errno;
+	int attempt;
+	int found;
+
+	ksft_print_header();
+	ksft_set_plan(1);
+
+	if (geteuid() != 0)
+		report_skip("requires root to set persistent_keyring_expiry");
+
+	if (read_expiry(&saved_expiry) < 0)
+		report_skip("CONFIG_PERSISTENT_KEYRINGS or procfs is unavailable");
+
+	found = find_persistent_expiry(test_uid, expiry, sizeof(expiry));
+	if (found < 0)
+		report_skip("/proc/keys is unavailable");
+	if (found) {
+		test_uid = 50000 + (getpid() % 10000);
+		for (attempt = 0; attempt < 128; attempt++) {
+			found = find_persistent_expiry(test_uid, expiry,
+						       sizeof(expiry));
+			if (found < 0)
+				report_skip("cannot read /proc/keys");
+			if (!found)
+				break;
+			test_uid++;
+		}
+		if (attempt == 128)
+			report_skip("could not find an unused persistent keyring UID");
+	}
+
+	/* The inherited session keyring may have been revoked. */
+	ret = syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL, 0, 0, 0);
+	if (ret < 0) {
+		set_failure(reason, sizeof(reason),
+			    "KEYCTL_JOIN_SESSION_KEYRING failed: %s",
+			    strerror(errno));
+		goto out;
+	}
+
+	if (write_expiry(TEST_EXPIRY) < 0) {
+		if (write_expiry(saved_expiry) < 0)
+			ksft_exit_fail_msg("failed to restore persistent keyring expiry: %s\n",
+					   strerror(errno));
+		report_skip("cannot change persistent_keyring_expiry");
+	}
+	restore_expiry = true;
+
+	{
+		char description[64];
+
+		snprintf(description, sizeof(description), "keyring-expiry-test-%d",
+			 getpid());
+		ret = add_keyring(description);
+	}
+	if (ret < 0) {
+		set_failure(reason, sizeof(reason), "add_key(keyring) failed: %s",
+			    strerror(errno));
+		goto out;
+	}
+	destination = ret;
+
+	ret = syscall(SYS_keyctl, KEYCTL_RESTRICT_KEYRING, destination,
+		      0, 0, 0);
+	if (ret < 0) {
+		set_failure(reason, sizeof(reason),
+			    "KEYCTL_RESTRICT_KEYRING failed: %s", strerror(errno));
+		goto out;
+	}
+
+	ret = syscall(SYS_keyctl, KEYCTL_GET_PERSISTENT, test_uid,
+		      destination, 0, 0);
+	get_persistent_errno = errno;
+	ksft_print_msg("GET_PERSISTENT returned %ld (%s)\n", ret,
+		       ret < 0 ? strerror(get_persistent_errno) : "success");
+	if (ret >= 0) {
+		linked_persistent = ret;
+		set_failure(reason, sizeof(reason),
+			    "GET_PERSISTENT unexpectedly linked key %ld", ret);
+		goto out;
+	}
+	if (get_persistent_errno != EPERM) {
+		set_failure(reason, sizeof(reason),
+			    "GET_PERSISTENT failed with %s instead of EPERM",
+			    strerror(get_persistent_errno));
+		goto out;
+	}
+
+	ret = find_persistent_expiry(test_uid, expiry, sizeof(expiry));
+	if (ret < 0) {
+		set_failure(reason, sizeof(reason), "cannot read /proc/keys");
+		goto out;
+	}
+	if (!ret) {
+		dump_persistent_keys();
+		set_failure(reason, sizeof(reason),
+			    "GET_PERSISTENT did not create the requested keyring");
+		skipped = true;
+		goto out;
+	}
+	if (!strcmp(expiry, "perm") || !strcmp(expiry, "expd")) {
+		set_failure(reason, sizeof(reason),
+			    "failed link left _persistent.%u with expiry %s",
+			    test_uid, expiry);
+		{
+			char description[64];
+
+			snprintf(description, sizeof(description),
+				 "keyring-expiry-cleanup-%d", getpid());
+			ret = add_keyring(description);
+		}
+		if (ret >= 0) {
+			cleanup_destination = ret;
+			ret = syscall(SYS_keyctl, KEYCTL_GET_PERSISTENT, test_uid,
+				      cleanup_destination, 0, 0);
+			if (ret >= 0) {
+				cleanup_persistent = ret;
+				if (unlink_key(cleanup_persistent,
+					       cleanup_destination) < 0) {
+					ksft_print_msg("warning: unlink temporary key: %s\n",
+						       strerror(errno));
+				} else {
+					cleanup_persistent = -1;
+				}
+			} else {
+				ksft_print_msg("warning: expiry cleanup failed: %s\n",
+					       strerror(errno));
+			}
+		} else {
+			ksft_print_msg("warning: unable to create cleanup keyring: %s\n",
+				       strerror(errno));
+		}
+		goto out;
+	}
+
+	passed = true;
+
+out:
+	if (linked_persistent > 0 &&
+	    unlink_key(linked_persistent, destination) < 0) {
+		ksft_print_msg("warning: unable to unlink persistent key from test keyring: %s\n",
+			       strerror(errno));
+		if (passed)
+			set_failure(reason, sizeof(reason),
+				    "unable to clean up linked persistent key: %s",
+				    strerror(errno));
+		passed = false;
+	}
+	if (cleanup_persistent > 0 && cleanup_destination > 0 &&
+	    unlink_key(cleanup_persistent, cleanup_destination) < 0) {
+		ksft_print_msg("warning: unable to unlink temporary persistent key: %s\n",
+			       strerror(errno));
+	}
+	if (cleanup_destination > 0 &&
+	    unlink_key(cleanup_destination, KEY_SPEC_SESSION_KEYRING) < 0) {
+		ksft_print_msg("warning: unable to unlink cleanup keyring: %s\n",
+			       strerror(errno));
+		if (passed)
+			set_failure(reason, sizeof(reason),
+				    "unable to clean up temporary keyring: %s",
+				    strerror(errno));
+		passed = false;
+	}
+	if (destination > 0 &&
+	    unlink_key(destination, KEY_SPEC_SESSION_KEYRING) < 0) {
+		ksft_print_msg("warning: unable to unlink test keyring: %s\n",
+			       strerror(errno));
+		if (passed)
+			set_failure(reason, sizeof(reason),
+				    "unable to clean up test keyring: %s",
+				    strerror(errno));
+		passed = false;
+	}
+	if (restore_expiry && write_expiry(saved_expiry) < 0) {
+		set_failure(reason, sizeof(reason),
+			    "failed to restore persistent_keyring_expiry: %s",
+			    strerror(errno));
+		passed = false;
+	}
+
+	if (passed) {
+		ksft_print_msg("restricted link returned EPERM; _persistent.%u expires in %s\n",
+			       test_uid, expiry);
+		ksft_test_result_pass("failed link still applies persistent keyring expiry\n");
+	} else if (skipped) {
+		ksft_test_result_skip("%s\n", reason);
+	} else {
+		ksft_test_result_fail("%s\n", reason);
+	}
+	ksft_finished();
+}
diff --git a/tools/testing/selftests/keys/run_qemu.sh b/tools/testing/selftests/keys/run_qemu.sh
new file mode 100755
index 000000000000..522cc8495ca2
--- /dev/null
+++ b/tools/testing/selftests/keys/run_qemu.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+set -eu
+
+script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
+repo_root=$(CDPATH='' cd -- "$script_dir/../../../.." && pwd)
+kernel_image=${1:-"$repo_root/arch/x86/boot/bzImage"}
+vmlinux=${2:-"$repo_root/vmlinux"}
+test_binary="$script_dir/persistent_keyring_expiry"
+
+for tool in cpio qemu-system-x86_64 busybox ldd; do
+	if ! command -v "$tool" >/dev/null 2>&1; then
+		echo "required tool not found: $tool" >&2
+		exit 1
+	fi
+done
+
+make -C "$script_dir"
+if [ ! -r "$kernel_image" ] || [ ! -r "$vmlinux" ]; then
+	echo "usage: $0 [bzImage] [vmlinux]" >&2
+	exit 1
+fi
+
+tmpdir=$(mktemp -d)
+trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
+rootfs="$tmpdir/rootfs"
+initrd="$tmpdir/initramfs.cpio"
+mkdir -p "$rootfs/bin" "$rootfs/dev" "$rootfs/proc" "$rootfs/keys"
+
+copy_binary()
+{
+	source=$1
+	destination=$2
+	install -D "$source" "$rootfs$destination"
+
+	ldd "$source" 2>/dev/null |
+		awk '$2 == "=>" && $3 ~ /^\// { print $3 }
+		     $1 ~ /^\// { print $1 }' |
+		sort -u |
+		while IFS= read -r library; do
+			[ -f "$library" ] || continue
+			cp -L --parents "$library" "$rootfs"
+		done
+}
+
+copy_binary "$(command -v busybox)" /bin/busybox
+copy_binary "$test_binary" /keys/persistent_keyring_expiry
+install -m 755 "$script_dir/initramfs-init.sh" "$rootfs/init"
+
+(
+	cd "$rootfs"
+	find . -print0 | cpio --null -o --format=newc
+) > "$initrd"
+
+echo "QEMU is paused at startup; attach GDB to localhost:1234 and continue."
+qemu-system-x86_64 \
+	-kernel "$kernel_image" \
+	-initrd "$initrd" \
+	-append "console=ttyS0 nokaslr rdinit=/init" \
+	-display none \
+	-serial stdio \
+	-monitor none \
+	-gdb tcp::1234 \
+	-S
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH] selftests/keys: Add persistent keyring expiry regression test
  2026-10-06 17:39 [PATCH] selftests/keys: Add persistent keyring expiry regression test Sahaj Chaudhari
@ 2026-10-08 14:57 ` Jarkko Sakkinen
  0 siblings, 0 replies; 2+ messages in thread
From: Jarkko Sakkinen @ 2026-10-08 14:57 UTC (permalink / raw)
  To: Sahaj Chaudhari; +Cc: shuah, linux-kselftest, linux-kernel, dhowells, keyrings

On Tue, Oct 06, 2026 at 11:09:02PM +0530, Sahaj Chaudhari wrote:
> KEYCTL_GET_PERSISTENT creates and registers a persistent keyring before
> linking it into the caller's destination keyring. If the destination is
> restricted, the link returns -EPERM. Verify that the keyring's configured
> expiry is still applied after the failed link.
> 
> Register the test in kselftest and document direct and QEMU/GDB execution.
> The failure case is intended for a disposable VM: an unexpired persistent
> keyring may remain registered until its user namespace is torn down.
> 
> Tested:
> 
>   make -C tools/testing/selftests/keys
> 
>   QEMU guest with fixed kernel: TAP pass 1/1
> 
> Signed-off-by: Sahaj Chaudhari <sahaj123.sc@gmail.com>

I don't think we want this. This is a specialized reproducer.

> ---
>  tools/testing/selftests/Makefile              |   1 +
>  tools/testing/selftests/keys/.gitignore       |   2 +
>  tools/testing/selftests/keys/Makefile         |   6 +
>  tools/testing/selftests/keys/README           |  50 +++
>  .../testing/selftests/keys/initramfs-init.sh  |  10 +
>  .../keys/persistent_keyring_expiry.c          | 367 ++++++++++++++++++
>  tools/testing/selftests/keys/run_qemu.sh      |  64 +++
>  7 files changed, 500 insertions(+)
>  create mode 100644 tools/testing/selftests/keys/.gitignore
>  create mode 100644 tools/testing/selftests/keys/Makefile
>  create mode 100644 tools/testing/selftests/keys/README
>  create mode 100755 tools/testing/selftests/keys/initramfs-init.sh
>  create mode 100644 tools/testing/selftests/keys/persistent_keyring_expiry.c
>  create mode 100755 tools/testing/selftests/keys/run_qemu.sh
> 
> diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
> index 273853937c25..d74ad9370bd1 100644
> --- a/tools/testing/selftests/Makefile
> +++ b/tools/testing/selftests/Makefile
> @@ -62,6 +62,7 @@ TARGETS += ipc
>  TARGETS += ir
>  TARGETS += kcmp
>  TARGETS += kexec
> +TARGETS += keys
>  TARGETS += kselftest_harness
>  TARGETS += kvm
>  TARGETS += landlock
> diff --git a/tools/testing/selftests/keys/.gitignore b/tools/testing/selftests/keys/.gitignore
> new file mode 100644
> index 000000000000..48c2900d780e
> --- /dev/null
> +++ b/tools/testing/selftests/keys/.gitignore
> @@ -0,0 +1,2 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +persistent_keyring_expiry
> diff --git a/tools/testing/selftests/keys/Makefile b/tools/testing/selftests/keys/Makefile
> new file mode 100644
> index 000000000000..33984d1eceb3
> --- /dev/null
> +++ b/tools/testing/selftests/keys/Makefile
> @@ -0,0 +1,6 @@
> +# SPDX-License-Identifier: GPL-2.0
> +CFLAGS += -g -Wall $(KHDR_INCLUDES)
> +
> +TEST_GEN_PROGS := persistent_keyring_expiry
> +
> +include ../lib.mk
> diff --git a/tools/testing/selftests/keys/README b/tools/testing/selftests/keys/README
> new file mode 100644
> index 000000000000..dd92abc1ddfe
> --- /dev/null
> +++ b/tools/testing/selftests/keys/README
> @@ -0,0 +1,50 @@
> +The persistent_keyring_expiry selftest verifies that a failed link from
> +KEYCTL_GET_PERSISTENT still gives a newly created persistent keyring its
> +configured expiry. It requires CONFIG_KEYS, CONFIG_PERSISTENT_KEYRINGS,
> +CONFIG_PROC_FS, procfs, and root privileges. The test temporarily changes
> +/proc/sys/kernel/keys/persistent_keyring_expiry and restores its original value.
> +
> +Build and run it against a kernel containing the fix with:
> +
> +    make -C tools/testing/selftests/keys
> +    sudo tools/testing/selftests/keys/persistent_keyring_expiry
> +
> +The QEMU/GDB instructions assume a configured Linux source tree with an
> +existing `.config` and the standard kernel build toolchain. The QEMU runner
> +requires `cpio`, `qemu-system-x86_64`, `busybox`, and `ldd`; GDB is required
> +for the interactive debugging steps.
> +
> +For a QEMU/GDB run, build an x86 kernel with debug symbols and
> +CONFIG_PERSISTENT_KEYRINGS=y. Starting from an existing `.config`, enable
> +the required options and build `bzImage` and `vmlinux`:
> +
> +    scripts/config --enable KEYS --enable PERSISTENT_KEYRINGS \
> +        --enable PROC_FS --enable DEVTMPFS --enable DEVTMPFS_MOUNT \
> +        --disable DEBUG_INFO_NONE --enable DEBUG_INFO \
> +        --enable DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT \
> +        --enable GDB_SCRIPTS --enable FRAME_POINTER
> +    make olddefconfig
> +    make -j2 bzImage
> +
> +Then run:
> +
> +    tools/testing/selftests/keys/run_qemu.sh path/to/bzImage path/to/vmlinux
> +
> +In another terminal, attach GDB and continue the paused guest:
> +
> +    gdb path/to/vmlinux
> +    (gdb) target remote localhost:1234
> +    (gdb) break key_get_persistent
> +    (gdb) break key_set_timeout
> +    (gdb) continue
> +
> +At `key_get_persistent`, inspect `uid` with `info args`. Continue until
> +`key_set_timeout` is hit, check `timeout` with `print timeout` (300 seconds),
> +then continue to let the test finish. The result is printed on the QEMU serial
> +console.
> +
> +To reproduce the bug, run this test against a kernel built from the parent of
> +commit 25bf14f81716. It reports a permanent ("perm") expiry. With the fix, the
> +restricted link returns -EPERM and the new keyring has a finite expiry.
> +The pre-fix bug can leave a permanent keyring in the test's user namespace
> +until that namespace is torn down, so run this reproduction in a disposable VM.

We would expect this to run without this documentatio existing at all.

> diff --git a/tools/testing/selftests/keys/initramfs-init.sh b/tools/testing/selftests/keys/initramfs-init.sh
> new file mode 100755
> index 000000000000..47cbfcb68d64
> --- /dev/null
> +++ b/tools/testing/selftests/keys/initramfs-init.sh
> @@ -0,0 +1,10 @@
> +#!/bin/busybox sh
> +# SPDX-License-Identifier: GPL-2.0
> +# shellcheck shell=dash
> +
> +/bin/busybox mount -t proc procfs /proc
> +/keys/persistent_keyring_expiry
> +status=$?
> +/bin/busybox echo "keyring expiry selftest exit status: $status"
> +/bin/busybox poweroff -f
> +exit "$status"
> diff --git a/tools/testing/selftests/keys/persistent_keyring_expiry.c b/tools/testing/selftests/keys/persistent_keyring_expiry.c
> new file mode 100644
> index 000000000000..55dbd8ce74ec
> --- /dev/null
> +++ b/tools/testing/selftests/keys/persistent_keyring_expiry.c
> @@ -0,0 +1,367 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#define _GNU_SOURCE
> +
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <linux/keyctl.h>
> +#include <limits.h>
> +#include <stdarg.h>
> +#include <stdbool.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/syscall.h>
> +#include <sys/types.h>
> +#include <unistd.h>
> +
> +#include "../kselftest.h"
> +
> +#define EXPIRY_PATH "/proc/sys/kernel/keys/persistent_keyring_expiry"
> +#define TEST_EXPIRY 300
> +
> +static int read_expiry(unsigned int *expiry)
> +{
> +	char buf[32];
> +	char *end;
> +	unsigned long value;
> +	ssize_t len;
> +	int fd;
> +
> +	fd = open(EXPIRY_PATH, O_RDONLY);
> +	if (fd < 0)
> +		return -1;
> +
> +	len = read(fd, buf, sizeof(buf) - 1);
> +	close(fd);
> +	if (len <= 0)
> +		return -1;
> +
> +	buf[len] = '\0';
> +	errno = 0;
> +	value = strtoul(buf, &end, 10);
> +	if (errno || end == buf || (*end != '\n' && *end != '\0') ||
> +	    value > UINT_MAX) {
> +		errno = EINVAL;
> +		return -1;
> +	}
> +
> +	*expiry = value;
> +	return 0;
> +}
> +
> +static int write_expiry(unsigned int expiry)
> +{
> +	char buf[32];
> +	size_t len;
> +	ssize_t written;
> +	int fd;
> +
> +	len = snprintf(buf, sizeof(buf), "%u\n", expiry);
> +	fd = open(EXPIRY_PATH, O_WRONLY);
> +	if (fd < 0)
> +		return -1;
> +
> +	written = write(fd, buf, len);
> +	if (written != len) {
> +		int saved_errno = errno;
> +
> +		close(fd);
> +		errno = written >= 0 ? EIO : saved_errno;
> +		return -1;
> +	}
> +	if (close(fd) < 0)
> +		return -1;
> +
> +	return 0;
> +}
> +
> +static long add_keyring(const char *description)
> +{
> +	return syscall(SYS_add_key, "keyring", description, NULL, 0,
> +		       KEY_SPEC_SESSION_KEYRING);
> +}
> +
> +static int unlink_key(int key, int keyring)
> +{
> +	return syscall(SYS_keyctl, KEYCTL_UNLINK, key, keyring, 0, 0);
> +}
> +
> +static int find_persistent_expiry(uid_t uid, char *expiry, size_t expiry_len)
> +{
> +	char description[32];
> +	char *line = NULL;
> +	size_t line_len = 0;
> +	ssize_t len;
> +	FILE *keys;
> +	int found = 0;
> +
> +	snprintf(description, sizeof(description), "_persistent.%u", uid);
> +	keys = fopen("/proc/keys", "r");
> +	if (!keys)
> +		return -1;
> +
> +	while ((len = getline(&line, &line_len, keys)) >= 0) {
> +		char *fields[9];
> +		char *saveptr;
> +		char *field;
> +		unsigned int n = 0;
> +		size_t description_len = strlen(description);
> +
> +		for (field = strtok_r(line, " \t\n", &saveptr);
> +		     field && n < ARRAY_SIZE(fields);
> +		     field = strtok_r(NULL, " \t\n", &saveptr))
> +			fields[n++] = field;
> +
> +		if (n == ARRAY_SIZE(fields) &&
> +		    strncmp(fields[8], description, description_len) == 0 &&
> +		    (fields[8][description_len] == '\0' ||
> +		     fields[8][description_len] == ':')) {
> +			snprintf(expiry, expiry_len, "%s", fields[3]);
> +			found = 1;
> +			break;
> +		}
> +	}
> +
> +	free(line);
> +	fclose(keys);
> +	return found;
> +}
> +
> +static void dump_persistent_keys(void)
> +{
> +	char *line = NULL;
> +	size_t line_len = 0;
> +	FILE *keys = fopen("/proc/keys", "r");
> +
> +	if (!keys)
> +		return;
> +
> +	while (getline(&line, &line_len, keys) >= 0) {
> +		if (strstr(line, "_persistent."))
> +			ksft_print_msg("visible key: %s", line);
> +	}
> +
> +	free(line);
> +	fclose(keys);
> +}
> +
> +static void set_failure(char *reason, size_t reason_len, const char *fmt, ...)
> +{
> +	va_list args;
> +
> +	va_start(args, fmt);
> +	vsnprintf(reason, reason_len, fmt, args);
> +	va_end(args);
> +}
> +
> +static void report_skip(const char *reason)
> +{
> +	ksft_test_result_skip("%s\n", reason);
> +	ksft_finished();
> +}
> +
> +int main(void)
> +{
> +	char expiry[32] = {};
> +	char reason[256] = {};
> +	unsigned int saved_expiry;
> +	uid_t test_uid = getuid();
> +	int destination = -1;
> +	int cleanup_destination = -1;
> +	int linked_persistent = -1;
> +	int cleanup_persistent = -1;
> +	long ret;
> +	bool restore_expiry = false;
> +	bool passed = false;
> +	bool skipped = false;
> +	int get_persistent_errno;
> +	int attempt;
> +	int found;
> +
> +	ksft_print_header();
> +	ksft_set_plan(1);
> +
> +	if (geteuid() != 0)
> +		report_skip("requires root to set persistent_keyring_expiry");
> +
> +	if (read_expiry(&saved_expiry) < 0)
> +		report_skip("CONFIG_PERSISTENT_KEYRINGS or procfs is unavailable");
> +
> +	found = find_persistent_expiry(test_uid, expiry, sizeof(expiry));
> +	if (found < 0)
> +		report_skip("/proc/keys is unavailable");
> +	if (found) {
> +		test_uid = 50000 + (getpid() % 10000);
> +		for (attempt = 0; attempt < 128; attempt++) {
> +			found = find_persistent_expiry(test_uid, expiry,
> +						       sizeof(expiry));
> +			if (found < 0)
> +				report_skip("cannot read /proc/keys");
> +			if (!found)
> +				break;
> +			test_uid++;
> +		}
> +		if (attempt == 128)
> +			report_skip("could not find an unused persistent keyring UID");
> +	}
> +
> +	/* The inherited session keyring may have been revoked. */
> +	ret = syscall(SYS_keyctl, KEYCTL_JOIN_SESSION_KEYRING, NULL, 0, 0, 0);
> +	if (ret < 0) {
> +		set_failure(reason, sizeof(reason),
> +			    "KEYCTL_JOIN_SESSION_KEYRING failed: %s",
> +			    strerror(errno));
> +		goto out;
> +	}
> +
> +	if (write_expiry(TEST_EXPIRY) < 0) {
> +		if (write_expiry(saved_expiry) < 0)
> +			ksft_exit_fail_msg("failed to restore persistent keyring expiry: %s\n",
> +					   strerror(errno));
> +		report_skip("cannot change persistent_keyring_expiry");
> +	}
> +	restore_expiry = true;
> +
> +	{
> +		char description[64];
> +
> +		snprintf(description, sizeof(description), "keyring-expiry-test-%d",
> +			 getpid());
> +		ret = add_keyring(description);
> +	}
> +	if (ret < 0) {
> +		set_failure(reason, sizeof(reason), "add_key(keyring) failed: %s",
> +			    strerror(errno));
> +		goto out;
> +	}
> +	destination = ret;
> +
> +	ret = syscall(SYS_keyctl, KEYCTL_RESTRICT_KEYRING, destination,
> +		      0, 0, 0);
> +	if (ret < 0) {
> +		set_failure(reason, sizeof(reason),
> +			    "KEYCTL_RESTRICT_KEYRING failed: %s", strerror(errno));
> +		goto out;
> +	}
> +
> +	ret = syscall(SYS_keyctl, KEYCTL_GET_PERSISTENT, test_uid,
> +		      destination, 0, 0);
> +	get_persistent_errno = errno;
> +	ksft_print_msg("GET_PERSISTENT returned %ld (%s)\n", ret,
> +		       ret < 0 ? strerror(get_persistent_errno) : "success");
> +	if (ret >= 0) {
> +		linked_persistent = ret;
> +		set_failure(reason, sizeof(reason),
> +			    "GET_PERSISTENT unexpectedly linked key %ld", ret);
> +		goto out;
> +	}
> +	if (get_persistent_errno != EPERM) {
> +		set_failure(reason, sizeof(reason),
> +			    "GET_PERSISTENT failed with %s instead of EPERM",
> +			    strerror(get_persistent_errno));
> +		goto out;
> +	}
> +
> +	ret = find_persistent_expiry(test_uid, expiry, sizeof(expiry));
> +	if (ret < 0) {
> +		set_failure(reason, sizeof(reason), "cannot read /proc/keys");
> +		goto out;
> +	}
> +	if (!ret) {
> +		dump_persistent_keys();
> +		set_failure(reason, sizeof(reason),
> +			    "GET_PERSISTENT did not create the requested keyring");
> +		skipped = true;
> +		goto out;
> +	}
> +	if (!strcmp(expiry, "perm") || !strcmp(expiry, "expd")) {
> +		set_failure(reason, sizeof(reason),
> +			    "failed link left _persistent.%u with expiry %s",
> +			    test_uid, expiry);
> +		{
> +			char description[64];
> +
> +			snprintf(description, sizeof(description),
> +				 "keyring-expiry-cleanup-%d", getpid());
> +			ret = add_keyring(description);
> +		}
> +		if (ret >= 0) {
> +			cleanup_destination = ret;
> +			ret = syscall(SYS_keyctl, KEYCTL_GET_PERSISTENT, test_uid,
> +				      cleanup_destination, 0, 0);
> +			if (ret >= 0) {
> +				cleanup_persistent = ret;
> +				if (unlink_key(cleanup_persistent,
> +					       cleanup_destination) < 0) {
> +					ksft_print_msg("warning: unlink temporary key: %s\n",
> +						       strerror(errno));
> +				} else {
> +					cleanup_persistent = -1;
> +				}
> +			} else {
> +				ksft_print_msg("warning: expiry cleanup failed: %s\n",
> +					       strerror(errno));
> +			}
> +		} else {
> +			ksft_print_msg("warning: unable to create cleanup keyring: %s\n",
> +				       strerror(errno));
> +		}
> +		goto out;
> +	}
> +
> +	passed = true;
> +
> +out:
> +	if (linked_persistent > 0 &&
> +	    unlink_key(linked_persistent, destination) < 0) {
> +		ksft_print_msg("warning: unable to unlink persistent key from test keyring: %s\n",
> +			       strerror(errno));
> +		if (passed)
> +			set_failure(reason, sizeof(reason),
> +				    "unable to clean up linked persistent key: %s",
> +				    strerror(errno));
> +		passed = false;
> +	}
> +	if (cleanup_persistent > 0 && cleanup_destination > 0 &&
> +	    unlink_key(cleanup_persistent, cleanup_destination) < 0) {
> +		ksft_print_msg("warning: unable to unlink temporary persistent key: %s\n",
> +			       strerror(errno));
> +	}
> +	if (cleanup_destination > 0 &&
> +	    unlink_key(cleanup_destination, KEY_SPEC_SESSION_KEYRING) < 0) {
> +		ksft_print_msg("warning: unable to unlink cleanup keyring: %s\n",
> +			       strerror(errno));
> +		if (passed)
> +			set_failure(reason, sizeof(reason),
> +				    "unable to clean up temporary keyring: %s",
> +				    strerror(errno));
> +		passed = false;
> +	}
> +	if (destination > 0 &&
> +	    unlink_key(destination, KEY_SPEC_SESSION_KEYRING) < 0) {
> +		ksft_print_msg("warning: unable to unlink test keyring: %s\n",
> +			       strerror(errno));
> +		if (passed)
> +			set_failure(reason, sizeof(reason),
> +				    "unable to clean up test keyring: %s",
> +				    strerror(errno));
> +		passed = false;
> +	}
> +	if (restore_expiry && write_expiry(saved_expiry) < 0) {
> +		set_failure(reason, sizeof(reason),
> +			    "failed to restore persistent_keyring_expiry: %s",
> +			    strerror(errno));
> +		passed = false;
> +	}
> +
> +	if (passed) {
> +		ksft_print_msg("restricted link returned EPERM; _persistent.%u expires in %s\n",
> +			       test_uid, expiry);
> +		ksft_test_result_pass("failed link still applies persistent keyring expiry\n");
> +	} else if (skipped) {
> +		ksft_test_result_skip("%s\n", reason);
> +	} else {
> +		ksft_test_result_fail("%s\n", reason);
> +	}
> +	ksft_finished();
> +}
> diff --git a/tools/testing/selftests/keys/run_qemu.sh b/tools/testing/selftests/keys/run_qemu.sh
> new file mode 100755
> index 000000000000..522cc8495ca2
> --- /dev/null
> +++ b/tools/testing/selftests/keys/run_qemu.sh
> @@ -0,0 +1,64 @@
> +#!/bin/sh
> +# SPDX-License-Identifier: GPL-2.0
> +set -eu
> +
> +script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
> +repo_root=$(CDPATH='' cd -- "$script_dir/../../../.." && pwd)
> +kernel_image=${1:-"$repo_root/arch/x86/boot/bzImage"}
> +vmlinux=${2:-"$repo_root/vmlinux"}
> +test_binary="$script_dir/persistent_keyring_expiry"
> +
> +for tool in cpio qemu-system-x86_64 busybox ldd; do
> +	if ! command -v "$tool" >/dev/null 2>&1; then
> +		echo "required tool not found: $tool" >&2
> +		exit 1
> +	fi
> +done
> +
> +make -C "$script_dir"
> +if [ ! -r "$kernel_image" ] || [ ! -r "$vmlinux" ]; then
> +	echo "usage: $0 [bzImage] [vmlinux]" >&2
> +	exit 1
> +fi
> +
> +tmpdir=$(mktemp -d)
> +trap 'rm -rf "$tmpdir"' EXIT HUP INT TERM
> +rootfs="$tmpdir/rootfs"
> +initrd="$tmpdir/initramfs.cpio"
> +mkdir -p "$rootfs/bin" "$rootfs/dev" "$rootfs/proc" "$rootfs/keys"
> +
> +copy_binary()
> +{
> +	source=$1
> +	destination=$2
> +	install -D "$source" "$rootfs$destination"
> +
> +	ldd "$source" 2>/dev/null |
> +		awk '$2 == "=>" && $3 ~ /^\// { print $3 }
> +		     $1 ~ /^\// { print $1 }' |
> +		sort -u |
> +		while IFS= read -r library; do
> +			[ -f "$library" ] || continue
> +			cp -L --parents "$library" "$rootfs"
> +		done
> +}
> +
> +copy_binary "$(command -v busybox)" /bin/busybox
> +copy_binary "$test_binary" /keys/persistent_keyring_expiry
> +install -m 755 "$script_dir/initramfs-init.sh" "$rootfs/init"
> +
> +(
> +	cd "$rootfs"
> +	find . -print0 | cpio --null -o --format=newc
> +) > "$initrd"
> +
> +echo "QEMU is paused at startup; attach GDB to localhost:1234 and continue."
> +qemu-system-x86_64 \
> +	-kernel "$kernel_image" \
> +	-initrd "$initrd" \
> +	-append "console=ttyS0 nokaslr rdinit=/init" \
> +	-display none \
> +	-serial stdio \
> +	-monitor none \
> +	-gdb tcp::1234 \
> +	-S

Normally you should not do this as the kselftest runs in the test target
in the first place.

> -- 
> 2.43.0
> 

Br, Jarkko

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-10-08 14:57 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-06 17:39 [PATCH] selftests/keys: Add persistent keyring expiry regression test Sahaj Chaudhari
2026-10-08 14:57 ` Jarkko Sakkinen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox