[RFC PATCH v2 3/3] selftests: prctl: add process-wide bounding-set drop tests

Jinjie Ruan ruanjinjie at huawei.com
Tue Sep 29 13:02:00 UTC 2026


Add tests for PR_CAPBSET_DROP_MASK covering:
- Argument validation
- Permission checking
- Process-wide application of the drop, including:
  - The calling thread
  - Blocked sibling threads
  - Threads created afterwards
  - Children forked by a sibling that has not materialized the drop yet
- Concurrent thread creation (clone() races)
- Concurrent callers with different masks and both words
  of the capability mask
- Ignore behavior for bits representing capabilities unknown to the kernel

Assisted-by: DeepSeek:DeepSeek-v4 flash
Signed-off-by: Jinjie Ruan <ruanjinjie at huawei.com>
---
 tools/testing/selftests/prctl/Makefile        |  12 +-
 .../selftests/prctl/cap-bset-drop-test.c      | 641 ++++++++++++++++++
 2 files changed, 649 insertions(+), 4 deletions(-)
 create mode 100644 tools/testing/selftests/prctl/cap-bset-drop-test.c

diff --git a/tools/testing/selftests/prctl/Makefile b/tools/testing/selftests/prctl/Makefile
index e770e86fad9a..583e8775be1f 100644
--- a/tools/testing/selftests/prctl/Makefile
+++ b/tools/testing/selftests/prctl/Makefile
@@ -1,14 +1,18 @@
 # SPDX-License-Identifier: GPL-2.0
-ifndef CROSS_COMPILE
 ARCH ?= $(shell uname -m 2>/dev/null || echo not)
 override ARCH := $(shell echo $(ARCH) | sed -e s/i.86/x86/ -e s/x86_64/x86/)
 
+TEST_GEN_PROGS := cap-bset-drop-test
+LDLIBS += -lpthread
+
+# The tests below are x86-only and embed x86 assembly, so they can only be
+# built when not cross-compiling.
+ifndef CROSS_COMPILE
 ifeq ($(ARCH),x86)
 TEST_PROGS := disable-tsc-ctxt-sw-stress-test disable-tsc-on-off-stress-test \
 		disable-tsc-test set-anon-vma-name-test set-process-name
 all: $(TEST_PROGS)
-
-include ../lib.mk
-
 endif
 endif
+
+include ../lib.mk
diff --git a/tools/testing/selftests/prctl/cap-bset-drop-test.c b/tools/testing/selftests/prctl/cap-bset-drop-test.c
new file mode 100644
index 000000000000..d0432f9b5837
--- /dev/null
+++ b/tools/testing/selftests/prctl/cap-bset-drop-test.c
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for PR_CAPBSET_DROP_MASK: argument validation, permission checking,
+ * and process-wide application of the bounding set drop to sibling threads
+ * and to threads created afterwards.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdatomic.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <linux/capability.h>
+
+#include "../kselftest.h"
+
+#ifndef PR_CAPBSET_DROP_MASK
+#define PR_CAPBSET_DROP_MASK 82
+#endif
+
+#define N_THREADS 4
+
+#define CHILD_PASS	0
+#define CHILD_FAIL	1
+#define CHILD_SKIP	2
+
+static int pipe_fd[2];
+static atomic_int threads_ready;
+static atomic_int threads_ok;
+static unsigned long dropped_cap;
+
+static atomic_int fork_go;
+static atomic_int fork_ok;
+
+static atomic_int conc_stop;
+static atomic_int conc_dropped;
+static atomic_int conc_bad;
+
+#define MULTI_DROP 8
+static int multi_caps[MULTI_DROP];
+static int multi_n;
+static pthread_barrier_t multi_start;
+static atomic_int multi_done;
+static atomic_int multi_bad;
+
+static int last_cap;
+
+static int read_last_cap(void)
+{
+	char buf[32];
+	int fd, n, v = -1;
+
+	fd = open("/proc/sys/kernel/cap_last_cap", O_RDONLY);
+	if (fd < 0)
+		return -1;
+	n = read(fd, buf, sizeof(buf) - 1);
+	close(fd);
+	if (n <= 0)
+		return -1;
+	buf[n] = '\0';
+	v = atoi(buf);
+	return v;
+}
+
+static int bset_has(unsigned long cap)
+{
+	return prctl(PR_CAPBSET_READ, cap, 0, 0, 0) > 0;
+}
+
+static int drop_cap(unsigned long cap)
+{
+	unsigned long low = cap < 32 ? 1UL << cap : 0;
+	unsigned long high = cap < 32 ? 0 : 1UL << (cap - 32);
+
+	return prctl(PR_CAPBSET_DROP_MASK, low, high, 0, 0);
+}
+
+static int run_child(int (*fn)(void))
+{
+	pid_t pid;
+	int status;
+
+	pid = fork();
+	if (pid < 0)
+		return CHILD_FAIL;
+	if (pid == 0)
+		_exit(fn());
+	if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status))
+		return CHILD_FAIL;
+	return WEXITSTATUS(status);
+}
+
+static void report_child(int ret, const char *name)
+{
+	if (ret == CHILD_PASS)
+		ksft_test_result_pass("%s\n", name);
+	else if (ret == CHILD_SKIP)
+		ksft_test_result_skip("%s\n", name);
+	else
+		ksft_test_result_fail("%s\n", name);
+}
+
+static void run_test(int cond, int (*fn)(void), const char *name)
+{
+	if (!cond) {
+		ksft_test_result_skip("%s\n", name);
+		return;
+	}
+	report_child(run_child(fn), name);
+}
+
+/*
+ * Return the first capability in [lo, hi] that is set in the bounding set,
+ * or -1 if there is none.
+ */
+static int pick_cap(int lo, int hi)
+{
+	int cap;
+
+	for (cap = lo; cap <= hi; cap++)
+		if (bset_has(cap))
+			return cap;
+	return -1;
+}
+
+static int have_cap_setpcap(void)
+{
+	struct __user_cap_header_struct hdr = {
+		.version = _LINUX_CAPABILITY_VERSION_3,
+	};
+	struct __user_cap_data_struct data[2];
+
+	if (syscall(SYS_capget, &hdr, data))
+		return 0;
+	return !!(data[0].effective & (1U << CAP_SETPCAP));
+}
+
+static int drop_effective_cap_setpcap(void)
+{
+	struct __user_cap_header_struct hdr = {
+		.version = _LINUX_CAPABILITY_VERSION_3,
+	};
+	struct __user_cap_data_struct data[2];
+
+	if (syscall(SYS_capget, &hdr, data))
+		return -1;
+	data[0].effective &= ~(1U << CAP_SETPCAP);
+	return syscall(SYS_capset, &hdr, data);
+}
+
+static void *blocked_worker(void *arg)
+{
+	char c;
+
+	(void)arg;
+
+	atomic_fetch_add(&threads_ready, 1);
+
+	/*
+	 * Block until the main thread has issued the drop.  The drop is made
+	 * effective for the whole group through the thread group's pending
+	 * mask, so it must be visible here once we run again.
+	 */
+	for (;;) {
+		ssize_t n = read(pipe_fd[0], &c, 1);
+
+		if (n == 1)
+			break;
+		if (n < 0 && errno == EINTR)
+			continue;
+		return NULL;
+	}
+
+	if (prctl(PR_CAPBSET_READ, dropped_cap, 0, 0, 0) == 0)
+		atomic_fetch_add(&threads_ok, 1);
+	return NULL;
+}
+
+static void *late_worker(void *arg)
+{
+	(void)arg;
+
+	/* Must inherit the reduced bounding set of the parent thread. */
+	if (prctl(PR_CAPBSET_READ, dropped_cap, 0, 0, 0) == 0)
+		atomic_fetch_add(&threads_ok, 1);
+	return NULL;
+}
+
+/*
+ * Child for the functional tests: drop @cap with PR_CAPBSET_DROP_MASK and
+ * verify that the calling thread, all blocked sibling threads and a thread
+ * created afterwards lose it from their bounding sets.
+ */
+static int drop_test_child(void)
+{
+	unsigned long cap = dropped_cap;
+	pthread_t t[N_THREADS], late;
+	char c = 'x';
+	int i, ret;
+
+	atomic_store(&threads_ready, 0);
+	atomic_store(&threads_ok, 0);
+
+	if (pipe(pipe_fd))
+		return CHILD_FAIL;
+
+	for (i = 0; i < N_THREADS; i++) {
+		if (pthread_create(&t[i], NULL, blocked_worker, NULL)) {
+			close(pipe_fd[0]);
+			close(pipe_fd[1]);
+			return CHILD_FAIL;
+		}
+	}
+	while (atomic_load(&threads_ready) < N_THREADS)
+		sched_yield();
+
+	ret = drop_cap(cap);
+	if (ret) {
+		ksft_print_msg("PR_CAPBSET_DROP_MASK(cap %lu) failed: %s\n",
+			       cap, strerror(errno));
+		ret = CHILD_FAIL;
+		goto out;
+	}
+
+	for (i = 0; i < N_THREADS; i++) {
+		if (write(pipe_fd[1], &c, 1) != 1) {
+			ksft_print_msg("pipe write failed: %s\n",
+				       strerror(errno));
+			/* Close the write end so the workers see EOF. */
+			close(pipe_fd[1]);
+			pipe_fd[1] = -1;
+			ret = CHILD_FAIL;
+			goto out_join;
+		}
+	}
+out_join:
+	for (i = 0; i < N_THREADS; i++)
+		pthread_join(t[i], NULL);
+
+	if (atomic_load(&threads_ok) != N_THREADS) {
+		ksft_print_msg("only %d of %d sibling threads saw the drop\n",
+			       atomic_load(&threads_ok), N_THREADS);
+		ret = CHILD_FAIL;
+		goto out;
+	}
+
+	/* The calling thread must have dropped it synchronously. */
+	if (bset_has(cap)) {
+		ksft_print_msg("calling thread still has capability %lu\n",
+			       cap);
+		ret = CHILD_FAIL;
+		goto out;
+	}
+
+	/* A thread created afterwards must inherit the reduced set. */
+	if (pthread_create(&late, NULL, late_worker, NULL))
+		goto out_ok;
+	pthread_join(late, NULL);
+	if (atomic_load(&threads_ok) != N_THREADS + 1) {
+		ksft_print_msg("late thread did not inherit the drop\n");
+		ret = CHILD_FAIL;
+		goto out;
+	}
+
+out_ok:
+	ret = CHILD_PASS;
+out:
+	if (pipe_fd[1] >= 0)
+		close(pipe_fd[1]);
+	close(pipe_fd[0]);
+	return ret;
+}
+
+/*
+ * A sibling thread that did not call PR_CAPBSET_DROP_MASK has not had its own
+ * cred updated, but it must still observe the drop and must pass the reduced
+ * bounding set to any child it forks.
+ */
+static void *fork_sibling(void *arg)
+{
+	pid_t pid;
+	int status;
+
+	(void)arg;
+
+	while (!atomic_load(&fork_go))
+		sched_yield();
+
+	if (bset_has(dropped_cap))
+		atomic_store(&fork_ok, 0);
+
+	pid = fork();
+	if (pid == 0)
+		_exit(bset_has(dropped_cap) ? 1 : 0);
+	if (pid < 0) {
+		atomic_store(&fork_ok, 0);
+		return NULL;
+	}
+	if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) ||
+	    WEXITSTATUS(status) != 0)
+		atomic_store(&fork_ok, 0);
+	return NULL;
+}
+
+static int fork_test_child(void)
+{
+	pthread_t sib;
+
+	atomic_store(&fork_go, 0);
+	atomic_store(&fork_ok, 1);
+
+	if (pthread_create(&sib, NULL, fork_sibling, NULL))
+		return CHILD_FAIL;
+
+	if (drop_cap(dropped_cap)) {
+		atomic_store(&fork_go, 1);
+		pthread_join(sib, NULL);
+		return CHILD_FAIL;
+	}
+	atomic_store(&fork_go, 1);
+	pthread_join(sib, NULL);
+
+	return atomic_load(&fork_ok) ? CHILD_PASS : CHILD_FAIL;
+}
+
+/*
+ * Threads created while the drop is in flight (or immediately after) must all
+ * observe it: the pending mask is shared by the whole group, so a thread can
+ * never be born with a capability that a concurrent drop removed.
+ */
+static void *conc_worker(void *arg)
+{
+	(void)arg;
+
+	while (!atomic_load(&conc_dropped))
+		sched_yield();
+	if (bset_has(dropped_cap))
+		atomic_fetch_add(&conc_bad, 1);
+	return NULL;
+}
+
+static void *conc_spawner(void *arg)
+{
+	(void)arg;
+
+	while (!atomic_load(&conc_stop)) {
+		pthread_t t;
+
+		if (pthread_create(&t, NULL, conc_worker, NULL) == 0)
+			pthread_join(t, NULL);
+	}
+	return NULL;
+}
+
+static int conc_test_child(void)
+{
+	pthread_t sp[4];
+	int i;
+
+	atomic_store(&conc_stop, 0);
+	atomic_store(&conc_dropped, 0);
+	atomic_store(&conc_bad, 0);
+
+	for (i = 0; i < 4; i++) {
+		if (pthread_create(&sp[i], NULL, conc_spawner, NULL))
+			return CHILD_FAIL;
+	}
+
+	if (drop_cap(dropped_cap)) {
+		atomic_store(&conc_stop, 1);
+		for (i = 0; i < 4; i++)
+			pthread_join(sp[i], NULL);
+		return CHILD_FAIL;
+	}
+	atomic_store(&conc_dropped, 1);
+	usleep(20000);
+	atomic_store(&conc_stop, 1);
+	for (i = 0; i < 4; i++)
+		pthread_join(sp[i], NULL);
+
+	return atomic_load(&conc_bad) ? CHILD_FAIL : CHILD_PASS;
+}
+
+/*
+ * Several threads invoke PR_CAPBSET_DROP_MASK concurrently with different
+ * masks while other threads are cloning.  The primitive is drop-only, so
+ * concurrent calls commute and the result is always the union of the requested
+ * drops, regardless of who "wins"; every thread, including ones created during
+ * the race, must end up without any of them.
+ */
+static int multi_bset_has_any(void)
+{
+	int i;
+
+	for (i = 0; i < multi_n; i++)
+		if (bset_has(multi_caps[i]))
+			return 1;
+	return 0;
+}
+
+static void *multi_reader(void *arg)
+{
+	(void)arg;
+
+	while (!atomic_load(&multi_done))
+		sched_yield();
+	if (multi_bset_has_any())
+		atomic_fetch_add(&multi_bad, 1);
+	return NULL;
+}
+
+static void *multi_spawner(void *arg)
+{
+	(void)arg;
+
+	while (!atomic_load(&multi_done)) {
+		pthread_t t;
+
+		if (pthread_create(&t, NULL, multi_reader, NULL) == 0)
+			pthread_join(t, NULL);
+	}
+	return NULL;
+}
+
+static void *multi_dropper(void *arg)
+{
+	long i = (long)arg;
+
+	pthread_barrier_wait(&multi_start);
+	/* Drop its own cap, then immediately race a second time. */
+	drop_cap(multi_caps[i]);
+	drop_cap(multi_caps[(i + 1) % multi_n]);
+	return NULL;
+}
+
+static int multi_drop_test_child(void)
+{
+	pthread_t dr[MULTI_DROP], sp[4], late;
+	int i;
+
+	multi_n = 0;
+	for (i = 0; i <= last_cap && multi_n < MULTI_DROP; i++) {
+		if (i == CAP_SETPCAP)
+			continue;
+		if (bset_has(i))
+			multi_caps[multi_n++] = i;
+	}
+	if (multi_n < 2)
+		return CHILD_SKIP;
+
+	atomic_store(&multi_done, 0);
+	atomic_store(&multi_bad, 0);
+	pthread_barrier_init(&multi_start, NULL, multi_n + 1);
+
+	for (i = 0; i < 4; i++) {
+		if (pthread_create(&sp[i], NULL, multi_spawner, NULL))
+			return CHILD_FAIL;
+	}
+	for (i = 0; i < multi_n; i++) {
+		if (pthread_create(&dr[i], NULL, multi_dropper,
+				   (void *)(long)i))
+			return CHILD_FAIL;
+	}
+	pthread_barrier_wait(&multi_start);
+	for (i = 0; i < multi_n; i++)
+		pthread_join(dr[i], NULL);
+	atomic_store(&multi_done, 1);
+	for (i = 0; i < 4; i++)
+		pthread_join(sp[i], NULL);
+
+	if (multi_bset_has_any()) /* the calling thread itself */
+		return CHILD_FAIL;
+	if (atomic_load(&multi_bad))
+		return CHILD_FAIL;
+
+	pthread_create(&late, NULL, multi_reader, NULL);
+	pthread_join(late, NULL);
+	if (atomic_load(&multi_bad))
+		return CHILD_FAIL;
+
+	return CHILD_PASS;
+}
+
+/*
+ * With CAP_SETPCAP dropped from the effective set, a non-empty mask must be
+ * rejected with EPERM.
+ */
+static int eperm_child(void)
+{
+	if (drop_effective_cap_setpcap()) {
+		ksft_print_msg("capset failed: %s\n", strerror(errno));
+		return CHILD_FAIL;
+	}
+
+	if (prctl(PR_CAPBSET_DROP_MASK, 1, 0, 0, 0) == 0 ||
+	    errno != EPERM) {
+		ksft_print_msg("PR_CAPBSET_DROP_MASK without CAP_SETPCAP: %s\n",
+			       strerror(errno));
+		return CHILD_FAIL;
+	}
+	return CHILD_PASS;
+}
+
+/*
+ * An empty mask is a no-op and must succeed even without CAP_SETPCAP: the
+ * "nothing to drop" check precedes the permission check.
+ */
+static int empty_child(void)
+{
+	if (drop_effective_cap_setpcap()) {
+		ksft_print_msg("capset failed: %s\n", strerror(errno));
+		return CHILD_FAIL;
+	}
+
+	if (prctl(PR_CAPBSET_DROP_MASK, 0, 0, 0, 0) != 0) {
+		ksft_print_msg("empty mask without CAP_SETPCAP: %s\n",
+			       strerror(errno));
+		return CHILD_FAIL;
+	}
+	return CHILD_PASS;
+}
+
+int main(void)
+{
+	int privileged = have_cap_setpcap();
+	int cap_lo, cap_hi;
+	unsigned long long unknown;
+
+	last_cap = read_last_cap();
+
+	ksft_print_header();
+	ksft_set_plan(10);
+
+	/* Argument validation, independent of privileges. */
+	if (prctl(PR_CAPBSET_DROP_MASK, 0, 0, 1, 0) == 0 ||
+	    errno != EINVAL)
+		ksft_test_result_fail("PR_CAPBSET_DROP_MASK nonzero arg4\n");
+	else
+		ksft_test_result_pass("PR_CAPBSET_DROP_MASK nonzero arg4\n");
+
+	if (prctl(PR_CAPBSET_DROP_MASK, 0, 0, 0, 1) == 0 ||
+	    errno != EINVAL)
+		ksft_test_result_fail("PR_CAPBSET_DROP_MASK nonzero arg5\n");
+	else
+		ksft_test_result_pass("PR_CAPBSET_DROP_MASK nonzero arg5\n");
+
+	/*
+	 * An empty mask is a no-op and succeeds even without CAP_SETPCAP,
+	 * because the emptiness check comes before the permission check.
+	 */
+	if (privileged) {
+		ksft_test_result(run_child(empty_child) == CHILD_PASS,
+				 "empty mask without CAP_SETPCAP\n");
+	} else if (prctl(PR_CAPBSET_DROP_MASK, 0, 0, 0, 0) != 0) {
+		ksft_test_result_fail("empty mask without CAP_SETPCAP (errno=%d, old kernel?)\n",
+				      errno);
+	} else {
+		ksft_test_result_pass("empty mask without CAP_SETPCAP\n");
+	}
+
+	/* A non-empty mask without CAP_SETPCAP must fail with EPERM. */
+	if (privileged) {
+		ksft_test_result(run_child(eperm_child) == CHILD_PASS,
+				 "EPERM without CAP_SETPCAP\n");
+	} else if (prctl(PR_CAPBSET_DROP_MASK, 1, 0, 0, 0) == 0 ||
+		   errno != EPERM) {
+		ksft_test_result_fail("EPERM without CAP_SETPCAP (errno=%d, old kernel?)\n",
+				      errno);
+	} else {
+		ksft_test_result_pass("EPERM without CAP_SETPCAP\n");
+	}
+
+	/* Bits for capabilities unknown to the kernel are silently ignored. */
+	unknown = (last_cap >= 0 && last_cap < 63) ? (~0ULL << (last_cap + 1)) : 0;
+	if (unknown) {
+		unsigned long low = (unsigned int)unknown;
+		unsigned long high = (unsigned int)(unknown >> 32);
+
+		if (!privileged) {
+			ksft_test_result_skip("unknown capabilities ignored (needs CAP_SETPCAP)\n");
+		} else if (prctl(PR_CAPBSET_DROP_MASK, low, high, 0, 0)) {
+			ksft_test_result_fail("unknown capabilities ignored (errno=%d)\n",
+					      errno);
+		} else {
+			ksft_test_result_pass("unknown capabilities ignored\n");
+		}
+	} else {
+		ksft_test_result_skip("unknown capabilities ignored\n");
+	}
+
+	/*
+	 * Functional tests, each in a child so the parent keeps its own
+	 * bounding set.  A low-word and a high-word capability cover the
+	 * arg2/arg3 split.
+	 */
+	cap_lo = last_cap >= 0 ? pick_cap(0, last_cap < 31 ? last_cap : 31) : -1;
+	cap_hi = last_cap > 31 ? pick_cap(32, last_cap) : -1;
+
+	dropped_cap = cap_lo;
+	run_test(privileged && cap_lo >= 0, drop_test_child,
+		 "PR_CAPBSET_DROP_MASK drops all threads");
+
+	dropped_cap = cap_hi;
+	run_test(privileged && cap_hi >= 0, drop_test_child,
+		 "PR_CAPBSET_DROP_MASK high word");
+
+	dropped_cap = cap_lo;
+
+	/*
+	 * A child forked by a sibling thread that has not materialized the
+	 * drop into its own cred must still inherit the reduced bounding set.
+	 */
+	run_test(privileged && cap_lo >= 0, fork_test_child,
+		 "PR_CAPBSET_DROP_MASK inherited by forked child");
+
+	/*
+	 * Threads created concurrently with the drop must all observe it.
+	 */
+	run_test(privileged && cap_lo >= 0, conc_test_child,
+		 "PR_CAPBSET_DROP_MASK concurrent threads");
+
+	/*
+	 * Concurrent callers with different masks, racing with thread
+	 * creation: the drop-only primitive must commute and the union of all
+	 * requested drops must be enforced on every thread.
+	 */
+	run_test(privileged, multi_drop_test_child,
+		 "PR_CAPBSET_DROP_MASK concurrent masks");
+
+	ksft_finished();
+}
-- 
2.34.1




More information about the Linux-security-module-archive mailing list