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