setuid / seteuid / uid / euid

setuid / seteuid / uid / euid

  • Written by
    Walter Doekes
  • Published on

So, what is the difference between uid and euid and the setuid and seteuid calls?

Hao Chen, David Wagner and Drew Dean wrote an excellent paper called Setuid Demystified. It explains all the ins and outs. To answer the question, we need only parts of the article.

Let the quoting begin.

Each process has three user IDs: the real user ID (real uid, or ruid), the effective user ID (effective uid, or euid), and the saved user ID (saved uid, or suid). The real uid identifies the owner of the process [i.e. the user launching the application], the effective uid is used in most access control decisions, and the saved uid stores a previous user ID so that it can be restored later.

[… A setuid binary starts with the file-owner in the euid and suid. See the examples below. …]

Similarly, a process has three group IDs: the real group ID, the effective group ID, and the saved group ID. In most cases, the properties of the group IDs parallel the properties of their user ID counterparts.

[… And, linux has two more for filesystem access: fsuid/fsgid. If not set explicitly, they follow the euid/egid. …]

As System V and BSD influenced each other, both systems implemented setuid, seteuid, and setreuid, although with different semantics. None of these system calls, however, allowed the direct manipulation of the saved uid (although it could be modified indirectly through setuid and setreuid). Therefore, some modern Unix systems introduced a new call, setresuid, to allow the modification of each of the three user IDs directly.

[…] setresuid has the clearest semantics among the four uid-setting system calls. The permission check for setresuid() is intuitive and common to all OSs: for the setresuid() system call to be allowed, either the euid of the process must be root, or each of the three parameters must be equal to one of the three user IDs of the process.

Requirements:
  (euid == 0)
    ||
  (newruid in (ruid, euid, suid) &&
   neweuid in (ruid, euid, suid) &&
   newsuid in (ruid, euid, suid))

[…] seteuid has also a clear semantics. It sets the effective uid while leaving the real uid and saved uid unchanged.

Requirements on Linux/Solaris:
  (euid == 0)
  ||
  (neweuid in (ruid, euid, suid))

Requirements on FreeBSD:
  (euid == 0)
  ||
  (neweuid in (ruid, suid))

[…] Although setuid is the only uid-setting system call standardized in POSIX 1003.1-1988, it is also the most confusing one.

Requirements on Linux/Solaris:
  (euid == 0)
  ||
  (newuid in (ruid, suid))

Requirements on FreeBSD:
  (euid == 0)
  ||
  (newuid in (ruid, euid, suid))

[…] Second, the action of setuid differs not only among different operating systems but also between privileged and unprivileged processes.

Behaviour on Linux/Solaris:
  (euid == 0) ==>
    (ruid:=newuid, euid:=newuid, suid:=newuid)
  (anything else) ==>
    (euid:=newuid)

Behaviour on FreeBSD:
  (ruid:=newuid, euid:=newuid, suid:=newuid)

Clear? Almost. seteuid seems to make sense, but the setuid behaviour seems rather awkward.

Maybe some examples on Linux help.

#define _GNU_SOURCE
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#define call(fun) errno = 0; fun; perror(#fun)
int main(int argc, char **argv) {
  uid_t ruid = -1, euid = -1, suid = -1;
  getresuid(&ruid, &euid, &suid);
  printf("> ruid=%d, euid=%d, suid=%d\n", ruid, euid, suid);
  call(setuid(1000));
  getresuid(&ruid, &euid, &suid);
  printf("> ruid=%d, euid=%d, suid=%d\n", ruid, euid, suid);
  return 0;
}
$ gcc example.c ; sudo chown root ./a.out ; sudo chmod 4755 ./a.out
$ ls -ln a.out
-rwsr-xr-x 1 0 1000 8716 2012-08-17 17:01 a.out
$ ./a.out
> ruid=1000, euid=0, suid=0
setuid(1000): Success
> ruid=1000, euid=1000, suid=1000

You were effectively root and you justed dropped those permissions. You should’ve used seteuid if you wanted them back.

$ sudo ./a.out
> ruid=0, euid=0, suid=0
setuid(1000): Success
> ruid=1000, euid=1000, suid=1000

Same as before.

Lets throw a third user in the mix.

$ sudo chown nobody ./a.out ; sudo chmod 4755 ./a.out
$ ls -ln a.out
-rwsr-xr-x 1 65534 1000 8716 2012-08-17 17:33 a.out
$ ./a.out
> ruid=1000, euid=65534, suid=65534
setuid(1000): Success
> ruid=1000, euid=1000, suid=65534

We’re really just doing seteuid here.

Lastly, the case where the euid isn’t 0 and newuid isn’t in real, effective or saved:

$ sudo ./a.out
> ruid=0, euid=65534, suid=65534
setuid(1000): Operation not permitted
> ruid=0, euid=65534, suid=65534

Yes, that didn’t satisfy the requirements as mentioned above. But we can seteuid(0) and then seteuid(1000) to get euid to 1000.

$ sudo ./a.out2
> ruid=0, euid=65534, suid=65534
seteuid(0): Success
seteuid(1000): Success
> ruid=0, euid=1000, suid=65534

Hope this helps ;-)

If you’re interested in the source of the awkward behaviour, read the entire article and/or one of their sources which goes into the cause much deeper: Chris Torek’s “setuid mess”

That should also explain why there are three uids and not just two. After all, two should be enough (effective and another one to switch back to), along with a pointer to the effective one: the early solution to temporarily drop powers was setreuid, a method by which two uids could be swapped. However, this swapping was cumbersome and was finally (skipping a few steps) replaced by setresuid.


Back to overview Newer post: easy / certificate generation / testing Older post: postfix / submission / smtpd_client_restrictions / sleep