/contrib/famzah

Enthusiasm never stops

Linux non-root user processes which run with group=root cannot change their process group to an arbitrary one

Leave a comment

Don’t be fooled like me, here is what the Linux kernel experts say regarding this matter:

There is no such thing as a “super-user group”. No group has any special privileges.

And also:

An effective group ID of zero does not accord any special privileges to change groups. This is a potential source of confusion: it is tempting to assume incorrectly that since appropriate privileges are carried by the euid in the setuid-like calls, they will be carried by the egid in the setgid-like calls, but this is not how it actually works.

You can review the whole thread with subject “EUID != root + EGID = root, and CAP_SETGID” at the Linux Kernel Mailing List.


If you run a Linux process with “user” privileges which are non-root and “group” privileges which are root, you will not be able to change the “group” of this process run-time by setgid() functions to an arbitrary group of the system.

I expected that if a process runs with “group” privileges set to “root”, then this process has the CAP_SETGID capability and thus is able to change its “group” to any group of the system. This turns out not to be the case. Such a process can only work with files which are accessible by the “root” group, just as it would have been if the “group” was not “root” but any other arbitrary group.

Why would I want to change the group to an arbitrary one? Because the process may start with “group” root, do some privileged work and then completely drop “group” root privileges to some non-root “group”, for security reasons.


I tested this on Linux kernel 2.6.31 but the situation for some previous versions was the same too:

famzah@famzahpc:~$ cat /proc/version
Linux version 2.6.31-14-generic (buildd@rothera) (gcc version 4.4.1 (Ubuntu 4.4.1-4ubuntu8) ) #48-Ubuntu SMP Fri Oct 16 14:04:26 UTC 2009

Note that a process can run with non-root “user” and a root “group” for one of the following reasons:

  • The process was started by the “root” super-user. This case is of no interest for this article.
  • The process was started by a “user” which has “root” defined for group in /etc/passwd, for example:

    testor:x:1003:0:,,,:/home/testor:/bin/bash

  • The process was started by a non-root “user” but the “group” owner of the file is root and this file has the set-group-ID on execution by “chmod g+s” permission. For example:

    -rwxr-sr-x 1 famzah root 10616 2009-12-11 11:17 a.out

Here is (one of) the corresponding code in the kernel which checks if a process can switch its running “group”:

setregid() in kernel/sys.c:
        if (egid != (gid_t) -1) {
                if ((old_rgid == egid) ||
                    (current->egid == egid) ||
                    (current->sgid == egid) ||
                    capable(CAP_SETGID))
                        new_egid = egid;
                else
                        return -EPERM;
        }

As you can see, even though a process may have root for “group”, it will not be able to change it to an arbitrary one if the process doesn’t have the CAP_SETGID capability.

I think that once a process gets an effective group ID which is root, this process must be granted the CAP_SETGID capability. I’ve sent a request for comment to the Linux Kernel Mailing List and I’m awaiting their reply on this matter.


You can easily test it yourself that a process running with group “root” doesn’t get the CAP_SETGID regardless if it was run with the set-group-ID file permission and “group” owner root, or by a non-root user which has root set for “group” in /etc/passwd.

Here are my results:

famzah@famzahpc:~$ ls -la a.out && ./a.out
-rwxr-xr-x 1 famzah famzah 8650 2009-12-11 12:06 a.out
RUID=1000, EUID=1000, SUID=1000
RGID=1000, EGID=1000, SGID=1000

Capabilities list returned by cap_to_text(): =
CAP_SETUID: CAP_EFFECTIVE=n CAP_INHERITABLE=n CAP_PERMITTED=n
CAP_SETGID: CAP_EFFECTIVE=n CAP_INHERITABLE=n CAP_PERMITTED=n


famzah@famzahpc:~$ ls -la a.out && ./a.out
-rwxr-sr-x 1 famzah root 8650 2009-12-11 12:06 a.out
RUID=1000, EUID=1000, SUID=1000
RGID=1000, EGID=0, SGID=0

Capabilities list returned by cap_to_text(): =
CAP_SETUID: CAP_EFFECTIVE=n CAP_INHERITABLE=n CAP_PERMITTED=n
CAP_SETGID: CAP_EFFECTIVE=n CAP_INHERITABLE=n CAP_PERMITTED=n


famzah@famzahpc:~$ cat /etc/passwd|grep testor
testor:x:1003:0:,,,:/home/testor:/bin/bash
famzah@famzahpc:~$ cp a.out /tmp/
famzah@famzahpc:~$ sudo -u testor /tmp/a.out
[sudo] password for famzah:
RUID=1003, EUID=1003, SUID=1003
RGID=0, EGID=0, SGID=0

Capabilities list returned by cap_to_text(): =
CAP_SETUID: CAP_EFFECTIVE=n CAP_INHERITABLE=n CAP_PERMITTED=n
CAP_SETGID: CAP_EFFECTIVE=n CAP_INHERITABLE=n CAP_PERMITTED=n


# Only if you are run by "user" root (or set-user-ID root),
# you can change your "group", because you gain CAP_SETGID.

famzah@famzahpc:~$ sudo -u root /tmp/a.out
RUID=0, EUID=0, SUID=0
RGID=0, EGID=0, SGID=0

Capabilities list returned by cap_to_text(): =ep
CAP_SETUID: CAP_EFFECTIVE=y CAP_INHERITABLE=n CAP_PERMITTED=y
CAP_SETGID: CAP_EFFECTIVE=y CAP_INHERITABLE=n CAP_PERMITTED=y

The source code of the capabilities dumper follows:

/* Compile with: gcc -Wall -lcap capdump.c */

#define _GNU_SOURCE
#include <sys/capability.h>
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

#define NUM_CAPS_TESTED 2

void manually_dump_caps(cap_t caps) {
        const cap_value_t cap_value_codes[NUM_CAPS_TESTED] = {CAP_SETUID, CAP_SETGID};
        const char *cap_value_names[NUM_CAPS_TESTED] = {"CAP_SETUID", "CAP_SETGID"};
        const cap_flag_t cap_flag_codes[3] = {CAP_EFFECTIVE, CAP_INHERITABLE, CAP_PERMITTED};
        const char *cap_flag_names[3] = {"CAP_EFFECTIVE", "CAP_INHERITABLE", "CAP_PERMITTED"};
        // -- //
        cap_flag_value_t flag_got_value;
        int i, j;

        for (i = 0; i < NUM_CAPS_TESTED; ++i) {
                printf("%s: ", cap_value_names[i]);
                for (j = 0; j < 3; ++j) {
                        if (cap_get_flag(caps, cap_value_codes[i], cap_flag_codes[j], &flag_got_value) != 0) {
                                err(EXIT_FAILURE, "cap_get_flag()");
                        }
                        printf("%s=%s ", cap_flag_names[j], (flag_got_value ? "y" : "n"));
                }
                printf("\n");
        }
}

void safe_cap_free(void *obj_d) {
        if (cap_free(obj_d) != 0) {
                err(EXIT_FAILURE, "cap_free()");
        }
}

int main() {
        cap_t caps;
        char *human_readable_s;
        uid_t ruid, euid, suid;
        gid_t rgid, egid, sgid;

        if (getresuid(&ruid, &euid, &suid) != 0) {
                err(EXIT_FAILURE, "getresuid()");
        }
        if (getresgid(&rgid, &egid, &sgid) != 0) {
                err(EXIT_FAILURE, "getresgid()");
        }

        printf("RUID=%d, EUID=%d, SUID=%d\n", ruid, euid, suid);
        printf("RGID=%d, EGID=%d, SGID=%d\n\n", rgid, egid, sgid);

        // -- //

        caps = cap_get_proc();
        if (caps == NULL) {
                err(EXIT_FAILURE, "cap_get_proc()");
        }

        human_readable_s = cap_to_text(caps, NULL /* do not store length */);
        if (human_readable_s == NULL) {
                err(EXIT_FAILURE, "cap_to_text()");
        }

        printf("Capabilities list returned by cap_to_text(): %s\n", human_readable_s);
        manually_dump_caps(caps);

        // -- //

        safe_cap_free(human_readable_s);
        safe_cap_free(caps);

        return 0;
}

Author: Ivan Zahariev

An experienced Linux & IT enthusiast, Engineer by heart, Systems architect & developer.

Leave a comment