Linux Security Modules development
 help / color / mirror / Atom feed
* [RFC PATCH v2 0/3] security: Add PR_CAPBSET_DROP_MASK
@ 2026-09-29 13:01 Jinjie Ruan
  2026-09-29 13:01 ` [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header Jinjie Ruan
                   ` (2 more replies)
  0 siblings, 3 replies; 8+ messages in thread
From: Jinjie Ruan @ 2026-09-29 13:01 UTC (permalink / raw)
  To: serge, kees, akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
	mingo, peterz, juri.lelli, vincent.guittot, dietmar.eggemann,
	rostedt, bsegall, mgorman, vschneid, kprateek.nayak, paul,
	jmorris, shuah, oleg, brauner, alexjlzheng, jannh, jaime.saguillo,
	elver, blbllhy, bvanassche, pjw, broonie, debug, tglx,
	aleksey.oladko, linux-kernel, linux-fsdevel,
	linux-security-module, linux-mm, linux-kselftest, morgan
  Cc: ruanjinjie

PR_CAPBSET_DROP only affects the calling thread, so dropping capabilities
for a whole process means one call per capability per thread.  For a
long-lived, multi-threaded process such as gVisor's sentry this is
stop-the-world signal delivery and costs milliseconds per sandbox on a
many-core host.

This series adds PR_CAPBSET_DROP_MASK, which removes a 64-bit mask of
capabilities from the whole thread group in a single call.  The drop is
recorded per thread group and folded into the bounding set wherever it
gates gaining a capability, so already-running, concurrently-created and
later-created threads -- as well as children forked by a sibling -- all
observe it.

Trimming 41 capabilities in an arm64 KVM guest goes from ~8.5-23.6ms with
the per-thread loop to ~11-14us, independent of the thread count.

Changes in RFC v2:
- Solve concurrently clone and concurrently drop mask problem.
- Solove sashiko problems in [1].
- Link to RFC v1: https://lore.kernel.org/all/20260922095816.1191799-1-ruanjinjie@huawei.com/
[1] https://sashiko.dev/#/patchset/20260922095816.1191799-1-ruanjinjie%40huawei.com

Jinjie Ruan (3):
  capability: Move mk_kernel_cap() to header
  security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops
  selftests: prctl: add process-wide bounding-set drop tests

 fs/proc/array.c                               |   3 +-
 include/linux/capability.h                    |  10 +
 include/linux/sched/signal.h                  |   8 +
 include/uapi/linux/prctl.h                    |   1 +
 kernel/capability.c                           |   5 -
 kernel/fork.c                                 |   1 +
 security/commoncap.c                          |  91 ++-
 tools/testing/selftests/prctl/Makefile        |  12 +-
 .../selftests/prctl/cap-bset-drop-test.c      | 641 ++++++++++++++++++
 9 files changed, 759 insertions(+), 13 deletions(-)
 create mode 100644 tools/testing/selftests/prctl/cap-bset-drop-test.c

-- 
2.34.1


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

* [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header
  2026-09-29 13:01 [RFC PATCH v2 0/3] security: Add PR_CAPBSET_DROP_MASK Jinjie Ruan
@ 2026-09-29 13:01 ` Jinjie Ruan
  2026-09-29 13:06   ` sashiko-bot
  2026-09-29 17:15   ` Bradley Morgan
  2026-09-29 13:01 ` [RFC PATCH v2 2/3] security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops Jinjie Ruan
  2026-09-29 13:02 ` [RFC PATCH v2 3/3] selftests: prctl: add process-wide bounding-set drop tests Jinjie Ruan
  2 siblings, 2 replies; 8+ messages in thread
From: Jinjie Ruan @ 2026-09-29 13:01 UTC (permalink / raw)
  To: serge, kees, akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
	mingo, peterz, juri.lelli, vincent.guittot, dietmar.eggemann,
	rostedt, bsegall, mgorman, vschneid, kprateek.nayak, paul,
	jmorris, shuah, oleg, brauner, alexjlzheng, jannh, jaime.saguillo,
	elver, blbllhy, bvanassche, pjw, broonie, debug, tglx,
	aleksey.oladko, linux-kernel, linux-fsdevel,
	linux-security-module, linux-mm, linux-kselftest, morgan
  Cc: ruanjinjie

Move mk_kernel_cap() from capability.c to the capability header file
so that it can be reused by other security modules.

No functional change.

Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>
---
 include/linux/capability.h | 5 +++++
 kernel/capability.c        | 5 -----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/include/linux/capability.h b/include/linux/capability.h
index f8532d92fcad..7921a0b3b04a 100644
--- a/include/linux/capability.h
+++ b/include/linux/capability.h
@@ -114,6 +114,11 @@ static inline bool cap_issubset(const kernel_cap_t a, const kernel_cap_t set)
 	return !(a.val & ~set.val);
 }
 
+static inline kernel_cap_t mk_kernel_cap(u32 low, u32 high)
+{
+	return (kernel_cap_t) { (low | ((u64)high << 32)) & CAP_VALID_MASK };
+}
+
 /* Used to decide between falling back on the old suser() or fsuser(). */
 
 static inline kernel_cap_t cap_drop_fs_set(const kernel_cap_t a)
diff --git a/kernel/capability.c b/kernel/capability.c
index 90e6ab62f6db..0b8ec26ead0e 100644
--- a/kernel/capability.c
+++ b/kernel/capability.c
@@ -190,11 +190,6 @@ SYSCALL_DEFINE2(capget, cap_user_header_t, header, cap_user_data_t, dataptr)
 	return 0;
 }
 
-static kernel_cap_t mk_kernel_cap(u32 low, u32 high)
-{
-	return (kernel_cap_t) { (low | ((u64)high << 32)) & CAP_VALID_MASK };
-}
-
 /**
  * sys_capset - set capabilities for a process or (*) a group of processes
  * @header: pointer to struct that contains capability version and
-- 
2.34.1


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

* [RFC PATCH v2 2/3] security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops
  2026-09-29 13:01 [RFC PATCH v2 0/3] security: Add PR_CAPBSET_DROP_MASK Jinjie Ruan
  2026-09-29 13:01 ` [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header Jinjie Ruan
@ 2026-09-29 13:01 ` Jinjie Ruan
  2026-09-29 13:19   ` sashiko-bot
  2026-09-29 13:02 ` [RFC PATCH v2 3/3] selftests: prctl: add process-wide bounding-set drop tests Jinjie Ruan
  2 siblings, 1 reply; 8+ messages in thread
From: Jinjie Ruan @ 2026-09-29 13:01 UTC (permalink / raw)
  To: serge, kees, akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
	mingo, peterz, juri.lelli, vincent.guittot, dietmar.eggemann,
	rostedt, bsegall, mgorman, vschneid, kprateek.nayak, paul,
	jmorris, shuah, oleg, brauner, alexjlzheng, jannh, jaime.saguillo,
	elver, blbllhy, bvanassche, pjw, broonie, debug, tglx,
	aleksey.oladko, linux-kernel, linux-fsdevel,
	linux-security-module, linux-mm, linux-kselftest, morgan
  Cc: ruanjinjie

PR_CAPBSET_DROP only affects the calling thread. Process-wide capability
dropping requires user space to loop over every thread and every
capability, which is highly expensive. For instance, long-lived
multi-threaded processes (like gVisor's sentry) spend milliseconds
trimming the bounding set because the runtime has to coordinate
and signal every thread.

Add PR_CAPBSET_DROP_MASK, an opt-in prctl that removes a set of
capabilities from the entire thread group's bounding set in a single call.
The capabilities are specified via a 64-bit mask across arg2 (low 32 bits)
and arg3 (high 32 bits).

The drop is recorded in a per-thread-group mask,
signal_struct::cap_bset_pending, under sighand->siglock. This pending drop
is dynamically folded into the bounding set in critical paths:
cap_capset(), PR_CAPBSET_READ(), and cap_bprm_creds_from_file().
Running, concurrently created, or future threads within the group will
all immediately observe the drop. A forked child inherits this mask
in cap_bset_drop_fork() under siglock to close the race window
against clone().

The operation is drop-only, so concurrent callers commute and a single
atomic OR is the linearization point.  It performs no per-thread allocation
and only the caller's replacement cred can fail, reported synchronously as
-ENOMEM before anything changes.

In an arm64 KVM guest, trimming 41 capabilities of a multi-threaded Go
process takes ~8.5-23.6ms with the per-thread PR_CAPBSET_DROP loop and
~11-14us with PR_CAPBSET_DROP_MASK, independent of the thread count.

Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>
---
 fs/proc/array.c              |  3 +-
 include/linux/capability.h   |  5 ++
 include/linux/sched/signal.h |  8 ++++
 include/uapi/linux/prctl.h   |  1 +
 kernel/fork.c                |  1 +
 security/commoncap.c         | 91 ++++++++++++++++++++++++++++++++++--
 6 files changed, 105 insertions(+), 4 deletions(-)

diff --git a/fs/proc/array.c b/fs/proc/array.c
index f6f75d206762..f1cde26d079c 100644
--- a/fs/proc/array.c
+++ b/fs/proc/array.c
@@ -63,6 +63,7 @@
 #include <linux/tty.h>
 #include <linux/string.h>
 #include <linux/mman.h>
+#include <linux/capability.h>
 #include <linux/sched/mm.h>
 #include <linux/sched/numa_balancing.h>
 #include <linux/sched/task_stack.h>
@@ -318,7 +319,7 @@ static inline void task_cap(struct seq_file *m, struct task_struct *p)
 	cap_inheritable	= cred->cap_inheritable;
 	cap_permitted	= cred->cap_permitted;
 	cap_effective	= cred->cap_effective;
-	cap_bset	= cred->cap_bset;
+	cap_bset	= cap_bset_effective(p, cred);
 	cap_ambient	= cred->cap_ambient;
 	rcu_read_unlock();
 
diff --git a/include/linux/capability.h b/include/linux/capability.h
index 7921a0b3b04a..6413c7fedb69 100644
--- a/include/linux/capability.h
+++ b/include/linux/capability.h
@@ -38,6 +38,7 @@ struct file;
 struct inode;
 struct dentry;
 struct task_struct;
+struct cred;
 struct user_namespace;
 struct mnt_idmap;
 
@@ -197,6 +198,10 @@ bool capable_wrt_inode_uidgid(struct mnt_idmap *idmap,
 			      const struct inode *inode, int cap);
 extern bool file_ns_capable(const struct file *file, struct user_namespace *ns, int cap);
 extern bool ptracer_capable(struct task_struct *tsk, struct user_namespace *ns);
+extern kernel_cap_t cap_bset_effective(const struct task_struct *task,
+				       const struct cred *cred);
+extern void cap_bset_drop_fork(struct task_struct *p);
+
 static inline bool perfmon_capable(void)
 {
 	return capable(CAP_PERFMON) || capable(CAP_SYS_ADMIN);
diff --git a/include/linux/sched/signal.h b/include/linux/sched/signal.h
index d45a5476b97d..bda6f18b65b9 100644
--- a/include/linux/sched/signal.h
+++ b/include/linux/sched/signal.h
@@ -98,6 +98,14 @@ struct signal_struct {
 	int			quick_threads;
 	struct list_head	thread_head;
 
+	/*
+	 * Capabilities being removed from the bounding set of every thread in
+	 * this group by PR_CAPBSET_DROP_MASK.  Read on capability-transition
+	 * paths without sighand->siglock, hence atomic64 (a 64-bit value would
+	 * otherwise tear on 32-bit).
+	 */
+	atomic64_t		cap_bset_pending;
+
 	wait_queue_head_t	wait_chldexit;	/* for wait4() */
 
 	/* current thread group signal load-balancing target: */
diff --git a/include/uapi/linux/prctl.h b/include/uapi/linux/prctl.h
index b6ec6f693719..85ac71897070 100644
--- a/include/uapi/linux/prctl.h
+++ b/include/uapi/linux/prctl.h
@@ -70,6 +70,7 @@
 /* Get/set the capability bounding set (as per security/commoncap.c) */
 #define PR_CAPBSET_READ 23
 #define PR_CAPBSET_DROP 24
+#define PR_CAPBSET_DROP_MASK 82
 
 /* Get/set the process' ability to use the timestamp counter instruction */
 #define PR_GET_TSC 25
diff --git a/kernel/fork.c b/kernel/fork.c
index 5ef413368912..bf671a85d39d 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -2504,6 +2504,7 @@ __latent_entropy struct task_struct *copy_process(
 	 * before holding sighand lock.
 	 */
 	copy_seccomp(p);
+	cap_bset_drop_fork(p);
 
 	if (clone_flags & CLONE_NNP)
 		task_set_no_new_privs(p);
diff --git a/security/commoncap.c b/security/commoncap.c
index 3399535808fe..3bec15d43731 100644
--- a/security/commoncap.c
+++ b/security/commoncap.c
@@ -3,6 +3,7 @@
  */
 
 #include <linux/capability.h>
+#include <linux/cred.h>
 #include <linux/audit.h>
 #include <linux/init.h>
 #include <linux/kernel.h>
@@ -19,6 +20,7 @@
 #include <linux/hugetlb.h>
 #include <linux/mount.h>
 #include <linux/sched.h>
+#include <linux/sched/signal.h>
 #include <linux/prctl.h>
 #include <linux/securebits.h>
 #include <linux/user_namespace.h>
@@ -30,6 +32,25 @@
 #define CREATE_TRACE_POINTS
 #include <trace/events/capability.h>
 
+/**
+ * Effective bounding set of @cred in @task's thread group
+ * @task: task whose thread group's pending drop applies
+ * @cred: credentials to read the bounding set from
+ *
+ * A drop recorded by PR_CAPBSET_DROP_MASK is authoritative on the thread group
+ * and may not have been materialized into every thread's cred yet, so the
+ * effective bounding set is the cred's own set minus the group's pending drop.
+ */
+kernel_cap_t cap_bset_effective(const struct task_struct *task,
+				const struct cred *cred)
+{
+	kernel_cap_t pending = {
+		.val = atomic64_read(&task->signal->cap_bset_pending),
+	};
+
+	return cap_drop(cred->cap_bset, pending);
+}
+
 /*
  * If a non-root user executes a setuid-root binary in
  * !secure(SECURE_NOROOT) mode, then we raise capabilities.
@@ -284,8 +305,8 @@ int cap_capset(struct cred *new,
 
 	if (!cap_issubset(*inheritable,
 			  cap_combine(old->cap_inheritable,
-				      old->cap_bset)))
 		/* no new pI capabilities outside bounding set */
+				      cap_bset_effective(current, old))))
 		return -EPERM;
 
 	/* verify restrictions on target's new Permitted set */
@@ -849,7 +870,7 @@ static void handle_privileged_root(struct linux_binprm *bprm, bool has_fcap,
 	 */
 	if (__is_eff(root_uid, new) || __is_real(root_uid, new)) {
 		/* pP' = (cap_bset & ~0) | (pI & ~0) */
-		new->cap_permitted = cap_combine(old->cap_bset,
+		new->cap_permitted = cap_combine(new->cap_bset,
 						 old->cap_inheritable);
 	}
 	/*
@@ -925,6 +946,8 @@ int cap_bprm_creds_from_file(struct linux_binprm *bprm, const struct file *file)
 	int ret;
 	kuid_t root_uid;
 
+	new->cap_bset = cap_bset_effective(current, new);
+
 	if (WARN_ON(!cap_ambient_invariant_ok(old)))
 		return -EPERM;
 
@@ -1283,6 +1306,63 @@ static int cap_prctl_drop(unsigned long cap)
 	return commit_creds(new);
 }
 
+static int cap_bset_drop_process(kernel_cap_t mask)
+{
+	kernel_cap_t pending;
+	struct cred *new;
+
+	new = prepare_creds();
+	if (!new)
+		return -ENOMEM;
+
+	/*
+	 * Record the drop before committing the caller's cred, so that a
+	 * thread created from now on is guaranteed to observe it.  Apply the
+	 * current union, not just this call's mask, to the caller's cred.
+	 */
+	spin_lock_irq(&current->sighand->siglock);
+	atomic64_or(mask.val, &current->signal->cap_bset_pending);
+	pending.val = atomic64_read(&current->signal->cap_bset_pending);
+	spin_unlock_irq(&current->sighand->siglock);
+
+	new->cap_bset = cap_drop(new->cap_bset, pending);
+	commit_creds(new);
+
+	return 0;
+}
+
+/*
+ * Propagate a pending process-wide bounding-set drop to @p, a task being
+ * created by the current thread.  Threads sharing the group read
+ * current->signal->cap_bset_pending directly; a forked child gets its own
+ * signal_struct and must carry the mask itself.  Called under
+ * current->sighand->siglock, which serializes it with cap_bset_drop_process().
+ */
+void cap_bset_drop_fork(struct task_struct *p)
+{
+	kernel_cap_t mask = {
+		.val = atomic64_read(&current->signal->cap_bset_pending),
+	};
+
+	if (cap_isclear(mask) || p->signal == current->signal)
+		return;
+
+	atomic64_or(mask.val, &p->signal->cap_bset_pending);
+}
+
+static int cap_prctl_drop_mask(unsigned long low, unsigned long high)
+{
+	kernel_cap_t mask = mk_kernel_cap((u32)low, (u32)high);
+
+	if (cap_isclear(mask))
+		return 0;
+
+	if (!ns_capable(current_user_ns(), CAP_SETPCAP))
+		return -EPERM;
+
+	return cap_bset_drop_process(mask);
+}
+
 /**
  * cap_task_prctl - Implement process control functions for this security module
  * @option: The process control function requested
@@ -1308,11 +1388,16 @@ int cap_task_prctl(int option, unsigned long arg2, unsigned long arg3,
 	case PR_CAPBSET_READ:
 		if (!cap_valid(arg2))
 			return -EINVAL;
-		return !!cap_raised(old->cap_bset, arg2);
+		return !!cap_raised(cap_bset_effective(current, old), arg2);
 
 	case PR_CAPBSET_DROP:
 		return cap_prctl_drop(arg2);
 
+	case PR_CAPBSET_DROP_MASK:
+		if (arg4 || arg5)
+			return -EINVAL;
+		return cap_prctl_drop_mask(arg2, arg3);
+
 	/*
 	 * The next four prctl's remain to assist with transitioning a
 	 * system from legacy UID=0 based privilege (when filesystem
-- 
2.34.1


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

* [RFC PATCH v2 3/3] selftests: prctl: add process-wide bounding-set drop tests
  2026-09-29 13:01 [RFC PATCH v2 0/3] security: Add PR_CAPBSET_DROP_MASK Jinjie Ruan
  2026-09-29 13:01 ` [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header Jinjie Ruan
  2026-09-29 13:01 ` [RFC PATCH v2 2/3] security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops Jinjie Ruan
@ 2026-09-29 13:02 ` Jinjie Ruan
  2026-09-29 13:11   ` sashiko-bot
  2 siblings, 1 reply; 8+ messages in thread
From: Jinjie Ruan @ 2026-09-29 13:02 UTC (permalink / raw)
  To: serge, kees, akpm, david, ljs, liam, vbabka, rppt, surenb, mhocko,
	mingo, peterz, juri.lelli, vincent.guittot, dietmar.eggemann,
	rostedt, bsegall, mgorman, vschneid, kprateek.nayak, paul,
	jmorris, shuah, oleg, brauner, alexjlzheng, jannh, jaime.saguillo,
	elver, blbllhy, bvanassche, pjw, broonie, debug, tglx,
	aleksey.oladko, linux-kernel, linux-fsdevel,
	linux-security-module, linux-mm, linux-kselftest, morgan
  Cc: ruanjinjie

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@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


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

* Re: [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header
  2026-09-29 13:01 ` [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header Jinjie Ruan
@ 2026-09-29 13:06   ` sashiko-bot
  2026-09-29 17:15   ` Bradley Morgan
  1 sibling, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-09-29 13:06 UTC (permalink / raw)
  Cc: linux-security-module

> Move mk_kernel_cap() from capability.c to the capability header file
> so that it can be reused by other security modules.
> 
> No functional change.
> 
> Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260929130200.1638343-1-ruanjinjie@huawei.com?part=1


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

* Re: [RFC PATCH v2 3/3] selftests: prctl: add process-wide bounding-set drop tests
  2026-09-29 13:02 ` [RFC PATCH v2 3/3] selftests: prctl: add process-wide bounding-set drop tests Jinjie Ruan
@ 2026-09-29 13:11   ` sashiko-bot
  0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-09-29 13:11 UTC (permalink / raw)
  Cc: linux-security-module

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] selftests: prctl: test fails to verify concurrent thread creation due to synchronous pthread_join
- [Medium] selftests: prctl: deadlock on drop_cap() error path in conc_test_child()
- [Medium] selftests: prctl: uninitialized thread ID passed to pthread_join() on pthread_create() failure
- [Medium] selftests: prctl: unmaterialized drop inheritance test is nullified by syscall before fork
--

commit efe86d5c1a9f1d9daea13c0d844c2621fc972d8d
Author: Jinjie Ruan <ruanjinjie@huawei.com>
Subject: selftests: prctl: add process-wide bounding-set drop tests

This commit adds tests for PR_CAPBSET_DROP_MASK. It covers argument validation,
permission checking, and process-wide application of the bounding set drop
across sibling threads and forked children.

> 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 0000000000000..d0432f9b58377
> --- /dev/null
> +++ b/tools/testing/selftests/prctl/cap-bset-drop-test.c

[ ... ]

> +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();

[Severity: Medium]
Does calling bset_has() here defeat the purpose of the test? bset_has() invokes
prctl(), which is a syscall. Returning to userspace from this syscall forces
pending task work (TIF_NOTIFY_RESUME) to be processed, fully materializing the
credentials before fork() occurs. This nullifies the intent of verifying that an
unmaterialized drop is properly inherited by a child thread.

> +	if (pid == 0)
> +		_exit(bset_has(dropped_cap) ? 1 : 0);

[ ... ]

> +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);

[Severity: Medium]
Does this synchronous wait serialize thread creation? Calling pthread_join()
immediately after pthread_create() blocks the spawner thread until the spawned
worker unblocks. As a result, no new threads are actively created during the
capability drop, nullifying the concurrent thread creation test.

> +	}
> +	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;
> +	}

[Severity: Medium]
Can this error path lead to an infinite hang? If drop_cap() fails, conc_dropped
is never set to 1. The worker threads will spin infinitely waiting for it to
change. This in turn causes the spawner threads to block infinitely in
pthread_join() waiting for the workers, and the main thread blocks infinitely
here waiting for the spawners.

> +	atomic_store(&conc_dropped, 1);
> +	usleep(20000);

[ ... ]

> +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);

[Severity: Medium]
Similar to the issue in conc_spawner(), does this synchronous wait prevent the
intended concurrency test? Waiting on the worker prevents thread creation races
with the concurrent capability drops.

> +	}
> +	return NULL;
> +}

[ ... ]

> +static int multi_drop_test_child(void)
> +{
> +	pthread_t dr[MULTI_DROP], sp[4], late;
> +	int i;

[ ... ]

> +	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);

[Severity: Medium]
Is it safe to unconditionally join this thread? If pthread_create() fails, late
remains uninitialized. Passing an uninitialized thread ID to pthread_join()
causes undefined behavior and could result in a crash or hang.

> +	if (atomic_load(&multi_bad))
> +		return CHILD_FAIL;
> +
> +	return CHILD_PASS;
> +}

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260929130200.1638343-1-ruanjinjie@huawei.com?part=3

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

* Re: [RFC PATCH v2 2/3] security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops
  2026-09-29 13:01 ` [RFC PATCH v2 2/3] security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops Jinjie Ruan
@ 2026-09-29 13:19   ` sashiko-bot
  0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-09-29 13:19 UTC (permalink / raw)
  Cc: linux-security-module

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] userns: PR_CAPBSET_DROP_MASK leaks into new user namespaces
- [High] cred: PR_CAPBSET_DROP_MASK breaks self-containment of struct cred
--

commit fcb774e0491f03726a243dd79d9b587551550d6e
Author: Jinjie Ruan <ruanjinjie@huawei.com>

security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops

This commit introduces a new prctl, PR_CAPBSET_DROP_MASK, to drop bounding set
capabilities across an entire thread group simultaneously. It achieves this by
storing a pending drop mask in the signal_struct and dynamically subtracting
it during capability checks.

> diff --git a/security/commoncap.c b/security/commoncap.c
> index 3399535808fec..3bec15d437312 100644
> --- a/security/commoncap.c
> +++ b/security/commoncap.c
[ ... ]
> @@ -30,6 +32,25 @@
>  #define CREATE_TRACE_POINTS
>  #include <trace/events/capability.h>
>  
> +/**
> + * Effective bounding set of @cred in @task's thread group
> + * @task: task whose thread group's pending drop applies
> + * @cred: credentials to read the bounding set from
> + *
> + * A drop recorded by PR_CAPBSET_DROP_MASK is authoritative on the thread group
> + * and may not have been materialized into every thread's cred yet, so the
> + * effective bounding set is the cred's own set minus the group's pending drop.
> + */
> +kernel_cap_t cap_bset_effective(const struct task_struct *task,
> +				const struct cred *cred)
> +{
> +	kernel_cap_t pending = {
> +		.val = atomic64_read(&task->signal->cap_bset_pending),
> +	};
> +
> +	return cap_drop(cred->cap_bset, pending);
> +}

[Severity: High]
Does this dynamic subtraction inadvertently leak parent bounding set drops into
new user namespaces?

When a new user namespace is created via clone(CLONE_NEWUSER) or unshare(),
set_cred_user_ns() resets cred->cap_bset to CAP_FULL_SET to ensure the new
namespace starts with full capabilities relative to itself.

However, because cap_bset_effective() dynamically subtracts the pending mask
from task->signal, any pending drops from the parent namespace will still be
applied to the new namespace's credentials. This could permanently restrict
them.

[ ... ]
> @@ -1283,6 +1306,63 @@ static int cap_prctl_drop(unsigned long cap)
>  	return commit_creds(new);
>  }
>  
> +static int cap_bset_drop_process(kernel_cap_t mask)
> +{
> +	kernel_cap_t pending;
> +	struct cred *new;
> +
> +	new = prepare_creds();
> +	if (!new)
> +		return -ENOMEM;
> +
> +	/*
> +	 * Record the drop before committing the caller's cred, so that a
> +	 * thread created from now on is guaranteed to observe it.  Apply the
> +	 * current union, not just this call's mask, to the caller's cred.
> +	 */
> +	spin_lock_irq(&current->sighand->siglock);
> +	atomic64_or(mask.val, &current->signal->cap_bset_pending);

[Severity: High]
Does storing the capability drop outside of struct cred break subsystems that
rely on the immutability and self-containment of saved credentials?

If a thread calls PR_CAPBSET_DROP_MASK, while another thread in the same
group opens an AppArmor-mediated file, that file saves the thread's stale
credential (e.g., in file->f_cred).

AppArmor might then evaluate the bounding set directly from this standalone
credential snapshot (like in is_subset_of_obj_privilege()). By placing the
drop state in signal_struct rather than struct cred, these asynchronous
evaluations will miss the pending drops.

Could this lead to false denials or security bypasses because the capability
evaluations are using stale capabilities?

> +	pending.val = atomic64_read(&current->signal->cap_bset_pending);
> +	spin_unlock_irq(&current->sighand->siglock);
> +
> +	new->cap_bset = cap_drop(new->cap_bset, pending);
> +	commit_creds(new);
> +
> +	return 0;
> +}
> +
> +/*
> + * Propagate a pending process-wide bounding-set drop to @p, a task being
> + * created by the current thread.  Threads sharing the group read
> + * current->signal->cap_bset_pending directly; a forked child gets its own
> + * signal_struct and must carry the mask itself.  Called under
> + * current->sighand->siglock, which serializes it with cap_bset_drop_process().
> + */
> +void cap_bset_drop_fork(struct task_struct *p)
> +{
> +	kernel_cap_t mask = {
> +		.val = atomic64_read(&current->signal->cap_bset_pending),
> +	};
> +
> +	if (cap_isclear(mask) || p->signal == current->signal)
> +		return;
> +
> +	atomic64_or(mask.val, &p->signal->cap_bset_pending);
> +}

[Severity: High]
Does actively copying the parent's dropped capabilities mask into a newly
created process erase namespace isolation boundaries?

When CLONE_NEWUSER is requested, the child is meant to receive a full
bounding set within its new namespace.

Because cap_bset_drop_fork() copies the parent's pending mask into the
child's signal_struct without clearing it for new user namespaces, those
capability drops will bleed into the child's effective bounding set. This
breaks the isolation guarantees of user namespaces.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260929130200.1638343-1-ruanjinjie@huawei.com?part=2

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

* Re: [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header
  2026-09-29 13:01 ` [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header Jinjie Ruan
  2026-09-29 13:06   ` sashiko-bot
@ 2026-09-29 17:15   ` Bradley Morgan
  1 sibling, 0 replies; 8+ messages in thread
From: Bradley Morgan @ 2026-09-29 17:15 UTC (permalink / raw)
  To: ruanjinjie
  Cc: akpm, aleksey.oladko, alexjlzheng, blbllhy, brauner, broonie,
	bsegall, bvanassche, david, debug, dietmar.eggemann, elver,
	jaime.saguillo, jannh, jmorris, juri.lelli, kees, kprateek.nayak,
	liam, linux-fsdevel, linux-kernel, linux-kselftest, linux-mm,
	linux-security-module, ljs, mgorman, mhocko, mingo, morgan, oleg,
	paul, peterz, pjw, rostedt, rppt, serge, shuah, surenb, tglx,
	vbabka, vincent.guittot, vschneid

On 29 September 2026 14:01:58 BST, Jinjie Ruan <ruanjinjie@huawei.com>
wrote:
>Move mk_kernel_cap() from capability.c to the capability header file
>so that it can be reused by other security modules.
>

Fair enoughh

Reviewed-by: Bradley Morgan <brads@mainlining.org>


>No functional change.
>
>Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>
>---
> include/linux/capability.h | 5 +++++
> kernel/capability.c        | 5 -----
> 2 files changed, 5 insertions(+), 5 deletions(-)
>
>diff --git a/include/linux/capability.h b/include/linux/capability.h
>index f8532d92fcad..7921a0b3b04a 100644
>--- a/include/linux/capability.h
>+++ b/include/linux/capability.h
>@@ -114,6 +114,11 @@ static inline bool cap_issubset(const kernel_cap_t a, const kernel_cap_t set)
> 	return !(a.val & ~set.val);
> }
> 
>+static inline kernel_cap_t mk_kernel_cap(u32 low, u32 high)
>+{
>+	return (kernel_cap_t) { (low | ((u64)high << 32)) & CAP_VALID_MASK };
>+}
>+
> /* Used to decide between falling back on the old suser() or fsuser(). */
> 
> static inline kernel_cap_t cap_drop_fs_set(const kernel_cap_t a)
>diff --git a/kernel/capability.c b/kernel/capability.c
>index 90e6ab62f6db..0b8ec26ead0e 100644
>--- a/kernel/capability.c
>+++ b/kernel/capability.c
>@@ -190,11 +190,6 @@ SYSCALL_DEFINE2(capget, cap_user_header_t, header, cap_user_data_t, dataptr)
> 	return 0;
> }
> 
>-static kernel_cap_t mk_kernel_cap(u32 low, u32 high)
>-{
>-	return (kernel_cap_t) { (low | ((u64)high << 32)) & CAP_VALID_MASK };
>-}
>-
> /**
>  * sys_capset - set capabilities for a process or (*) a group of processes
>  * @header: pointer to struct that contains capability version and
>

--- Thanks!
"I'm not a very positive person" - Linus torvalds

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

end of thread, other threads:[~2026-09-29 17:15 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-29 13:01 [RFC PATCH v2 0/3] security: Add PR_CAPBSET_DROP_MASK Jinjie Ruan
2026-09-29 13:01 ` [RFC PATCH v2 1/3] capability: Move mk_kernel_cap() to header Jinjie Ruan
2026-09-29 13:06   ` sashiko-bot
2026-09-29 17:15   ` Bradley Morgan
2026-09-29 13:01 ` [RFC PATCH v2 2/3] security: Add PR_CAPBSET_DROP_MASK for process-wide bounding-set drops Jinjie Ruan
2026-09-29 13:19   ` sashiko-bot
2026-09-29 13:02 ` [RFC PATCH v2 3/3] selftests: prctl: add process-wide bounding-set drop tests Jinjie Ruan
2026-09-29 13:11   ` sashiko-bot

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