SafeSetID: GID transition policy lookup uses RUID as source, allowlist can be bypassed (setgid to any group including 0)
Yan Yanhx
yanhx7 at gmail.com
Sun Aug 30 16:18:23 UTC 2026
Hi,
Summary
-------
In security/safesetid/lsm.c, id_permitted_for_cred() builds the policy
lookup key for a GID transition from the process's old RUID instead of its
old RGID. When a process's real UID value differs from every source GID in
the installed GID policy, setgid()/setregid()/setresgid() to any group
outside the allowlist is silently permitted, and the process is not
killed. I reproduced setgid(999) and setgid(0) succeeding on Linux 7.2.
Affected version
----------------
Introduced by commit 5294bac97e12 ("LSM: SafeSetID: Add GID security
policy handling", 2020-07-16), first shipped in v5.10. Still present in
current master (08dbfad3f504). The vulnerable line is unchanged since
introduction.
Code path
---------
security/safesetid/lsm.c:
static bool id_permitted_for_cred(const struct cred *old, kid_t new_id,
enum setid_type new_type)
{
...
if (new_type == GID) {
if (gid_eq(new_id.gid, old->gid) || ...)
return true;
}
...
permitted =
setid_policy_lookup((kid_t){.uid = old->uid}, new_id, new_type)
!= SIDPOL_CONSTRAINED;
The lookup key uses old->uid unconditionally. kid_t is a union of kuid_t
and kgid_t, so for new_type == GID the numeric value of the old RUID is
reinterpreted as the source GID and looked up in safesetid_setgid_rules.
The guard in safesetid_task_fix_setgid() (which decides whether the process
is constrained) correctly uses old->gid.
Consequence: if the old RUID is not numerically equal to any source GID of
the installed policy, the lookup misses and returns SIDPOL_DEFAULT, so the
transition is treated as allowed. setgid() to any group - including gid 0 -
then succeeds without the intended SIGKILL. This defeats the GID allowlist
that SafeSetID is designed to enforce.
Reproduction
------------
Environment: Linux 7.2 (v7.2 tag, kernel.org), CONFIG_SECURITY_SAFESETID=y,
booted with lsm=...,safesetid.
Deploy a GID policy that allows exactly one transition:
# echo "2000:100" > /sys/kernel/security/safesetid/gid_allowlist_policy
The policy means: from source gid 2000, switching to gid 100 is allowed;
anything else must be blocked.
Then, as a process with credentials RUID=1000, RGID=2000, holding
CAP_SETGID:
setgid(999);
Control group (RUID == source gid, so the bug is not triggered):
RUID=2000, RGID=2000, CAP_SETGID, setgid(999) -> process killed
(SIGKILL, exit 137), as intended.
Buggy case (RUID != source gid):
RUID=1000, RGID=2000, CAP_SETGID, setgid(999) -> succeeds (exit 0)
RUID=1000, RGID=2000, CAP_SETGID, setgid(0) -> succeeds (exit 0)
Both out-of-allowlist transitions succeed when the old RUID is not a
policy source GID. A minimal reproducer program is available on request;
per the reporting guidelines for AI-assisted findings I am not posting the
exploit source in this message. Say the word and I will send it privately.
Fix (tested)
------------
The fix builds the policy lookup source from the GID when the transition
is a GID one, and keeps the UID otherwise:
permitted =
setid_policy_lookup(new_type == UID ?
(kid_t){.uid = old->uid} : (kid_t){.gid = old->gid},
new_id, new_type) != SIDPOL_CONSTRAINED;
I built this change into Linux 7.2 and re-ran the reproduction:
before fix:
RUID=1000, RGID=2000, setgid(999) -> succeeds (exit 0) [bypass]
RUID=1000, RGID=2000, setgid(0) -> succeeds (exit 0) [bypass]
after fix:
RUID=1000, RGID=2000, setgid(999) -> killed (exit 137)
RUID=1000, RGID=2000, setgid(0) -> killed (exit 137)
RUID=2000, RGID=2000, setgid(100) -> succeeds (exit 0)
[allowed, unchanged]
The out-of-allowlist transitions are now blocked and the legitimate
transition still succeeds. scripts/checkpatch.pl reports 0 errors,
0 warnings. If a proper patch is wanted, I can prepare a git
format-patch with Fixes: 5294bac97e12 ("LSM: SafeSetID: Add GID
security policy handling"); the change above is offered as a starting
point for the maintainers, not as a submission.
Impact
------
Deployments that rely on SafeSetID GID policies to confine a CAP_SETGID
service to a limited set of target groups lose that confinement: the
process can switch to any group, including the root group. This is an
authorization-boundary bypass limited to systems that explicitly enable
SafeSetID and install a GID policy; it is not a privilege escalation from
an unprivileged context on a default kernel.
Best regards
More information about the Linux-security-module-archive
mailing list