* [PATCH 1/2] security: safesetid: use real GID for GID policy lookup
2026-10-03 10:34 [PATCH 0/2] SafeSetID: use real GID for GID policy lookups tjdqudcks0424
@ 2026-10-03 10:34 ` tjdqudcks0424
2026-10-04 22:05 ` Serge E. Hallyn
2026-10-03 10:34 ` [PATCH 2/2] selftests/safesetid: test GID policy with mismatched real IDs tjdqudcks0424
1 sibling, 1 reply; 4+ messages in thread
From: tjdqudcks0424 @ 2026-10-03 10:34 UTC (permalink / raw)
To: Micah Morton, Paul Moore, linux-security-module
Cc: James Morris, Serge E . Hallyn, Thomas Cedeno, Shuah Khan,
Christian Brauner, linux-kselftest, linux-kernel, security
From: Sung Byeongchan <tjdqudcks0424@naver.com>
SafeSetID policies constrain the ID transitions available to a task for
each source ID. id_permitted_for_cred() always builds that source key
from the old real UID, even when it is checking a GID transition. If the
real UID and real GID differ, the policy attached to the real GID is
therefore missed and the lookup can return the unconstrained default.
On current mainline, a test task with real UID 1000, real GID 2000 and
only CAP_SETGID obtained non-allowlisted GID 2002 despite a 2000:2001 GID
policy. The bypass reproduced for setgid(), setegid(), setregid(),
setresgid(), setfsgid() and setgroups() across three clean QEMU boots,
and enabled access to a synthetic group-protected resource. This shows
a SafeSetID GID policy bypass and group privilege expansion; it does not
show direct UID 0 elevation or a universal local privilege escalation.
Build the source key from the old real UID for UID policy checks and the
old real GID for GID policy checks. In three fixed-kernel boots, all
tested non-allowlisted transitions were blocked while the allowed target,
existing-ID, no-policy and UID-policy controls, and the existing SafeSetID
selftest continued to pass.
Fixes: 5294bac97e12 ("LSM: SafeSetID: Add GID security policy handling")
Cc: stable@vger.kernel.org
Assisted-by: OpenAI Codex
Signed-off-by: Sung Byeongchan <tjdqudcks0424@naver.com>
---
security/safesetid/lsm.c | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/security/safesetid/lsm.c b/security/safesetid/lsm.c
index d5fb949050dd8..18124fc499dbc 100644
--- a/security/safesetid/lsm.c
+++ b/security/safesetid/lsm.c
@@ -148,13 +148,16 @@ static int safesetid_security_capable(const struct cred *cred,
static bool id_permitted_for_cred(const struct cred *old, kid_t new_id, enum setid_type new_type)
{
bool permitted;
+ kid_t source_id;
/* If our old creds already had this ID in it, it's fine. */
if (new_type == UID) {
+ source_id.uid = old->uid;
if (uid_eq(new_id.uid, old->uid) || uid_eq(new_id.uid, old->euid) ||
uid_eq(new_id.uid, old->suid))
return true;
} else if (new_type == GID){
+ source_id.gid = old->gid;
if (gid_eq(new_id.gid, old->gid) || gid_eq(new_id.gid, old->egid) ||
gid_eq(new_id.gid, old->sgid))
return true;
@@ -162,11 +165,10 @@ static bool id_permitted_for_cred(const struct cred *old, kid_t new_id, enum set
return false;
/*
- * Transitions to new UIDs require a check against the policy of the old
- * RUID.
+ * Transitions require a check against the policy of the old real ID.
*/
permitted =
- setid_policy_lookup((kid_t){.uid = old->uid}, new_id, new_type) != SIDPOL_CONSTRAINED;
+ setid_policy_lookup(source_id, new_id, new_type) != SIDPOL_CONSTRAINED;
if (!permitted) {
if (new_type == UID) {
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH 2/2] selftests/safesetid: test GID policy with mismatched real IDs
2026-10-03 10:34 [PATCH 0/2] SafeSetID: use real GID for GID policy lookups tjdqudcks0424
2026-10-03 10:34 ` [PATCH 1/2] security: safesetid: use real GID for GID policy lookup tjdqudcks0424
@ 2026-10-03 10:34 ` tjdqudcks0424
1 sibling, 0 replies; 4+ messages in thread
From: tjdqudcks0424 @ 2026-10-03 10:34 UTC (permalink / raw)
To: Micah Morton, Paul Moore, linux-security-module
Cc: James Morris, Serge E . Hallyn, Thomas Cedeno, Shuah Khan,
Christian Brauner, linux-kselftest, linux-kernel, security
From: Sung Byeongchan <tjdqudcks0424@naver.com>
The existing test uses equal numeric UID and GID values, so it cannot
distinguish which real ID SafeSetID uses as the source of a GID policy
lookup.
Add isolated child-process cases with real UID 1000, real GID 2000 and a
2000:2001 GID policy. Verify that 2001 is allowed and 2002 is rejected
for both setresgid() and setgroups(). Each child retains only CAP_SETGID
in its permitted and effective sets, and pidfd polling bounds every case
without timing the transition.
On an affected kernel, the forbidden transitions survive and make the
test fail. With the fix, SafeSetID terminates those children and the test
passes.
Assisted-by: OpenAI Codex
Signed-off-by: Sung Byeongchan <tjdqudcks0424@naver.com>
---
MAINTAINERS | 1 +
tools/testing/selftests/safesetid/Makefile | 4 +-
.../safesetid/safesetid-gid-policy-test.c | 252 ++++++++++++++++++
.../safesetid/safesetid-gid-policy-test.sh | 11 +
4 files changed, 266 insertions(+), 2 deletions(-)
create mode 100644 tools/testing/selftests/safesetid/safesetid-gid-policy-test.c
create mode 100755 tools/testing/selftests/safesetid/safesetid-gid-policy-test.sh
diff --git a/MAINTAINERS b/MAINTAINERS
index 7235a92ff879b..68b355ad33e0f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24266,6 +24266,7 @@ M: Micah Morton <mortonm@chromium.org>
S: Supported
F: Documentation/admin-guide/LSM/SafeSetID.rst
F: security/safesetid/
+F: tools/testing/selftests/safesetid/
SAMSUNG AUDIO (ASoC) DRIVERS
M: Sylwester Nawrocki <s.nawrocki@samsung.com>
diff --git a/tools/testing/selftests/safesetid/Makefile b/tools/testing/selftests/safesetid/Makefile
index e815bbf2d0f4a..1a4a6630858c0 100644
--- a/tools/testing/selftests/safesetid/Makefile
+++ b/tools/testing/selftests/safesetid/Makefile
@@ -3,7 +3,7 @@
CFLAGS = -Wall -O2
LDLIBS = -lcap
-TEST_PROGS := safesetid-test.sh
-TEST_GEN_FILES := safesetid-test
+TEST_PROGS := safesetid-test.sh safesetid-gid-policy-test.sh
+TEST_GEN_FILES := safesetid-test safesetid-gid-policy-test
include ../lib.mk
diff --git a/tools/testing/selftests/safesetid/safesetid-gid-policy-test.c b/tools/testing/selftests/safesetid/safesetid-gid-policy-test.c
new file mode 100644
index 0000000000000..873d65627c494
--- /dev/null
+++ b/tools/testing/selftests/safesetid/safesetid-gid-policy-test.c
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <grp.h>
+#include <linux/capability.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/capability.h>
+#include <sys/prctl.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define TEST_UID 1000
+#define POLICY_GID 2000
+#define ALLOWED_GID 2001
+#define FORBIDDEN_GID 2002
+
+#define GID_POLICY_FILE \
+ "/sys/kernel/security/safesetid/gid_allowlist_policy"
+#define GID_POLICY "2000:2001\n"
+
+enum test_operation {
+ OP_SETRESGID,
+ OP_SETGROUPS,
+};
+
+static int install_policy(void)
+{
+ FILE *policy;
+ int ret = 0;
+
+ policy = fopen(GID_POLICY_FILE, "w");
+ if (!policy) {
+ perror("fopen gid policy");
+ return -1;
+ }
+ if (fputs(GID_POLICY, policy) == EOF) {
+ perror("write gid policy");
+ ret = -1;
+ }
+ if (fclose(policy)) {
+ perror("close gid policy");
+ ret = -1;
+ }
+ return ret;
+}
+
+static int retain_only_setgid(void)
+{
+ cap_value_t cap = CAP_SETGID;
+ cap_t caps;
+ int ret = -1;
+
+ caps = cap_get_proc();
+ if (!caps) {
+ perror("cap_get_proc");
+ return -1;
+ }
+ if (cap_clear(caps) ||
+ cap_set_flag(caps, CAP_PERMITTED, 1, &cap, CAP_SET) ||
+ cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap, CAP_SET) ||
+ cap_set_proc(caps)) {
+ perror("retain CAP_SETGID");
+ goto out;
+ }
+ ret = 0;
+out:
+ cap_free(caps);
+ return ret;
+}
+
+static int check_child_credentials(void)
+{
+ struct __user_cap_header_struct header = {
+ .version = _LINUX_CAPABILITY_VERSION_3,
+ };
+ struct __user_cap_data_struct data[_LINUX_CAPABILITY_U32S_3] = {};
+ uint64_t effective, permitted, inheritable;
+ uid_t ruid, euid, suid;
+ gid_t rgid, egid, sgid;
+ gid_t groups[2];
+ uint64_t expected = 1ULL << CAP_SETGID;
+ int i, ngroups;
+
+ if (getresuid(&ruid, &euid, &suid) ||
+ getresgid(&rgid, &egid, &sgid)) {
+ perror("getresuid/getresgid");
+ return -1;
+ }
+ if (ruid != TEST_UID || euid != TEST_UID || suid != TEST_UID ||
+ rgid != POLICY_GID || egid != POLICY_GID || sgid != POLICY_GID) {
+ fprintf(stderr, "unexpected child IDs\n");
+ return -1;
+ }
+
+ ngroups = getgroups(2, groups);
+ if (ngroups != 1 || groups[0] != POLICY_GID) {
+ fprintf(stderr, "unexpected initial supplementary groups\n");
+ return -1;
+ }
+ if (syscall(SYS_capget, &header, data)) {
+ perror("capget");
+ return -1;
+ }
+ effective = data[0].effective | ((uint64_t)data[1].effective << 32);
+ permitted = data[0].permitted | ((uint64_t)data[1].permitted << 32);
+ inheritable = data[0].inheritable | ((uint64_t)data[1].inheritable << 32);
+ if (effective != expected || permitted != expected || inheritable) {
+ fprintf(stderr, "child does not hold only CAP_SETGID\n");
+ return -1;
+ }
+ for (i = 0; i <= CAP_LAST_CAP; i++) {
+ if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, i, 0, 0) != 0) {
+ fprintf(stderr, "unexpected ambient capability %d\n", i);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int prepare_child(void)
+{
+ gid_t groups[] = { POLICY_GID };
+
+ if (setgroups(1, groups)) {
+ perror("initial setgroups");
+ return -1;
+ }
+ if (setresgid(POLICY_GID, POLICY_GID, POLICY_GID)) {
+ perror("setresgid policy source");
+ return -1;
+ }
+ if (prctl(PR_SET_KEEPCAPS, 1L)) {
+ perror("PR_SET_KEEPCAPS");
+ return -1;
+ }
+ if (setresuid(TEST_UID, TEST_UID, TEST_UID)) {
+ perror("setresuid test user");
+ return -1;
+ }
+ if (retain_only_setgid())
+ return -1;
+ return check_child_credentials();
+}
+
+static void run_child(enum test_operation operation, gid_t target)
+{
+ gid_t groups[] = { target };
+ int ret;
+
+ if (prepare_child())
+ _exit(2);
+
+ if (operation == OP_SETRESGID)
+ ret = setresgid(target, target, target);
+ else
+ ret = setgroups(1, groups);
+
+ if (ret) {
+ fprintf(stderr, "%s(%u) returned %s\n",
+ operation == OP_SETRESGID ? "setresgid" : "setgroups",
+ target, strerror(errno));
+ _exit(3);
+ }
+ _exit(0);
+}
+
+static int run_case(const char *name, enum test_operation operation,
+ gid_t target, bool expect_kill)
+{
+ struct pollfd pollfd = {};
+ int status;
+ int pidfd;
+ pid_t child;
+
+ child = fork();
+ if (child < 0) {
+ perror("fork");
+ return -1;
+ }
+ if (!child)
+ run_child(operation, target);
+
+ pidfd = syscall(SYS_pidfd_open, child, 0);
+ if (pidfd < 0) {
+ perror("pidfd_open");
+ kill(child, SIGKILL);
+ waitpid(child, &status, 0);
+ return -1;
+ }
+ pollfd.fd = pidfd;
+ pollfd.events = POLLIN;
+ if (poll(&pollfd, 1, 5000) != 1) {
+ kill(child, SIGKILL);
+ waitpid(child, &status, 0);
+ close(pidfd);
+ fprintf(stderr, "not ok - %s (timeout)\n", name);
+ return -1;
+ }
+ close(pidfd);
+ if (waitpid(child, &status, 0) < 0) {
+ perror("waitpid");
+ return -1;
+ }
+
+ if (expect_kill) {
+ if (WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) {
+ printf("ok - %s\n", name);
+ return 0;
+ }
+ fprintf(stderr, "not ok - %s (forbidden transition survived)\n",
+ name);
+ return -1;
+ }
+
+ if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
+ printf("ok - %s\n", name);
+ return 0;
+ }
+ fprintf(stderr, "not ok - %s (allowed transition failed)\n", name);
+ return -1;
+}
+
+int main(void)
+{
+ int failures = 0;
+
+ if (getuid()) {
+ fprintf(stderr, "must be run as root\n");
+ return 4;
+ }
+ if (install_policy())
+ return 1;
+
+ failures += run_case("allowed GID transition", OP_SETRESGID,
+ ALLOWED_GID, false) != 0;
+ failures += run_case("forbidden GID transition", OP_SETRESGID,
+ FORBIDDEN_GID, true) != 0;
+ failures += run_case("allowed supplementary group", OP_SETGROUPS,
+ ALLOWED_GID, false) != 0;
+ failures += run_case("forbidden supplementary group", OP_SETGROUPS,
+ FORBIDDEN_GID, true) != 0;
+
+ return failures ? 1 : 0;
+}
diff --git a/tools/testing/selftests/safesetid/safesetid-gid-policy-test.sh b/tools/testing/selftests/safesetid/safesetid-gid-policy-test.sh
new file mode 100755
index 0000000000000..7318ad3403121
--- /dev/null
+++ b/tools/testing/selftests/safesetid/safesetid-gid-policy-test.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+ksft_skip=4
+
+if [ "$(id -u)" -ne 0 ]; then
+ echo "safesetid-gid-policy-test: must be run as root" >&2
+ exit "$ksft_skip"
+fi
+
+exec ./safesetid-gid-policy-test
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread