The Principle of Least Privilege
The principle of least privilege states that a program should hold only the permissions it actually needs at any given moment โ no more, no less. For privileged programs, this translates to a clear rule: drop privilege when you don’t need it, re-raise it only when you do, and drop it permanently once you’ll never need it again.
This is not optional advice. It is the single most important practice for secure privileged programming. If your program is running with root privilege while doing something it doesn’t need root for (reading user input, parsing config files, etc.), any bug in that code becomes a root-level security hole.
The saved set-user-ID is the key that enables the “drop and re-raise” pattern. When a set-UID-root program starts, the kernel sets all three UIDs:
| State | Real UID | Effective UID | Saved Set-UID | Note |
|---|---|---|---|---|
| Program starts | 1000 | 0 | 0 | Full root access |
Drop privilegeseteuid(getuid()) |
1000 | 1000 | 0 | Unprivileged work. Saved UID=0 preserved for later. |
Re-raise privilegeseteuid(orig_euid) |
1000 | 0 | 0 | Back to root for privileged work |
Permanent dropsetuid(getuid()) when EUID=0 |
1000 | 1000 | 1000 | All three UIDs reset. Cannot regain privilege. |
The critical insight: seteuid() can change the effective UID between real UID and saved set-UID, but it doesn’t change the saved set-UID itself. That’s what keeps the “re-raise” option available.
The standard pattern for temporarily dropping privilege uses seteuid(). The saved set-UID acts as a “bookmark” so you can restore the original privilege later.
The best practice is: drop privilege immediately at program startup, then re-acquire it only for the exact operations that need it, then drop it again immediately after.
The Pattern
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
/* Save the original effective UID at program start */
uid_t orig_euid;
void drop_privilege(void)
{
/* seteuid(getuid()) sets EUID = RUID
* The saved set-UID is NOT changed โ we can come back */
if (seteuid(getuid()) == -1) {
perror("seteuid: drop");
exit(EXIT_FAILURE);
}
}
void raise_privilege(void)
{
/* seteuid(orig_euid) restores EUID from saved set-UID */
if (seteuid(orig_euid) == -1) {
perror("seteuid: raise");
exit(EXIT_FAILURE);
}
}
int main(void)
{
/* Step 1: Save original EUID before doing anything */
orig_euid = geteuid();
/* Step 2: Drop privilege immediately at startup โ BEST PRACTICE */
drop_privilege();
printf("[Unprivileged] Doing normal work (parsing config, reading stdin...)\n");
printf(" Current EUID: %d\n", (int)geteuid());
/* Step 3: Re-raise privilege for the specific operation that needs it */
raise_privilege();
printf("[Privileged] Opening /etc/shadow or binding port 80...\n");
printf(" Current EUID: %d\n", (int)geteuid());
/* ... privileged operation here ... */
/* Step 4: Drop again immediately */
drop_privilege();
printf("[Unprivileged] Back to safe mode.\n");
return 0;
}
Once your program is done with all privileged operations, it must permanently drop privileges. This means resetting all three UIDs to the real UID โ including the saved set-UID. If the saved set-UID still holds a privileged value, an attacker who tricks the program (e.g., via stack crashing) might be able to call seteuid() to regain root.
For Set-UID-root Programs (EUID=0)
When the effective UID is currently 0, setuid(getuid()) resets ALL three user IDs (real, effective, saved) because this behavior is triggered specifically when the caller is root:
/* When called with EUID = 0, setuid() resets all three UIDs */
if (setuid(getuid()) == -1) {
perror("setuid: permanent drop");
exit(EXIT_FAILURE);
}
/* Now: real=1000, effective=1000, saved=1000 โ TRULY unprivileged */
โ ๏ธ The Common Mistake โ WRONG sequence!
Many programmers make this fatal error: they temporarily drop privilege first (setting EUID to real UID), and THEN call setuid(getuid()). This looks correct but does NOT reset the saved set-UID:
/* BUG: This does NOT permanently drop root! */
/* Initial state: real=1000, effective=0, saved=0 */
orig_euid = geteuid(); /* orig_euid = 0 */
seteuid(getuid()); /* State: real=1000, effective=1000, saved=0 */
/* NOW calling setuid() โ EUID is no longer 0, so it only changes EUID! */
setuid(getuid()); /* State UNCHANGED: real=1000, effective=1000, saved=0 */
/* DANGER: saved=0 is still there. An attacker can call seteuid(0) to regain root! */
The fix is simple: if you’ve temporarily dropped privilege, re-raise it first to EUID=0, then call setuid(getuid()):
/* CORRECT: Re-raise to root first, THEN drop permanently */
seteuid(orig_euid); /* EUID back to 0: real=1000, effective=0, saved=0 */
setuid(getuid()); /* Now setuid() is called as root โ resets ALL UIDs */
/* Result: real=1000, effective=1000, saved=1000 โ truly permanent */
For Non-Root Set-UID Programs
If the program is set-UID but owned by a non-root user (say, UID=200), setuid() alone is not enough. You must use setreuid() or setresuid():
/* Use setreuid() to also reset the saved set-UID for non-root SUID programs */
/* Linux: when ruid argument is not -1, saved set-UID is set to new EUID */
if (setreuid(getuid(), getuid()) == -1) {
perror("setreuid");
exit(EXIT_FAILURE);
}
/* On Linux: real=1000, effective=1000, saved=1000 */
/* Or better โ use setresuid() for clear, explicit semantics */
uid_t uid = getuid();
if (setresuid(uid, uid, uid) == -1) {
perror("setresuid");
exit(EXIT_FAILURE);
}
It’s not enough to just call the system call and hope it worked. Always verify the result with a follow-up getresuid() or geteuid() call. On some systems, or when capabilities are manipulated, the credential change can silently fail or partially succeed.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void permanently_drop_root(void)
{
uid_t uid = getuid();
uid_t ruid, euid, suid;
/* Step 1: Make sure we're calling as root so setuid resets all UIDs */
if (geteuid() != 0) {
/* If we previously dropped, re-raise first */
if (seteuid(0) == -1) {
perror("seteuid re-raise");
exit(EXIT_FAILURE);
}
}
/* Step 2: Drop all UIDs permanently */
if (setuid(uid) == -1) {
perror("setuid permanent drop");
exit(EXIT_FAILURE);
}
/* Step 3: VERIFY the drop worked โ critical step! */
if (getresuid(&ruid, &euid, &suid) == -1) {
perror("getresuid verify");
exit(EXIT_FAILURE);
}
if (ruid != uid || euid != uid || suid != uid) {
fprintf(stderr, "FATAL: privilege drop failed! ruid=%d euid=%d suid=%d\n",
(int)ruid, (int)euid, (int)suid);
exit(EXIT_FAILURE); /* Safest option: terminate */
}
/* Step 4: Double-check that we cannot regain root */
if (seteuid(0) != -1) {
fprintf(stderr, "FATAL: was able to re-raise to root after permanent drop!\n");
exit(EXIT_FAILURE);
}
printf("[OK] Privilege permanently dropped. ruid=%d euid=%d suid=%d\n",
(int)ruid, (int)euid, (int)suid);
}
int main(void)
{
printf("Before drop: EUID=%d\n", (int)geteuid());
permanently_drop_root();
printf("After drop: EUID=%d\n", (int)geteuid());
return 0;
}
Everything discussed above applies equally to group IDs. Set-GID programs use the same three-ID model (real GID, effective GID, saved set-GID), and the same drop/re-raise/permanent-drop patterns apply using setegid(), setgid(), setresgid().
Important rule: When dropping multiple IDs (supplementary groups, group IDs, user IDs), always drop the privileged effective user ID last. Similarly, when raising IDs, raise the privileged effective user ID first. This is because some operations (like setting supplementary groups) may require root privilege, and you can’t do them after dropping root.
#include <unistd.h>
#include <grp.h>
/* Correct order when DROPPING multiple IDs */
void drop_all_ids_correctly(void)
{
uid_t uid = getuid();
gid_t gid = getgid();
/* 1. Drop supplementary groups first (needs root) */
if (setgroups(0, NULL) == -1) {
perror("setgroups");
exit(EXIT_FAILURE);
}
/* 2. Drop group IDs (still have root EUID) */
if (setresgid(gid, gid, gid) == -1) {
perror("setresgid");
exit(EXIT_FAILURE);
}
/* 3. Drop user ID LAST */
if (setresuid(uid, uid, uid) == -1) {
perror("setresuid");
exit(EXIT_FAILURE);
}
/* Always verify */
}
๐ Key Terms
seteuid() changes only the effective UID, leaving the saved set-UID intact โ privilege can be re-raised later. setuid() when called with EUID=0 (root) resets all three UIDs (real, effective, saved), making the drop permanent and irreversible.
seteuid(getuid()), the EUID becomes the real UID (non-root). Then setuid() called with non-root EUID only changes the EUID, leaving the saved set-UID=0. An attacker can still call seteuid(0) to regain root.
setresuid(uid, uid, uid) is preferred because it explicitly sets all three UIDs (real, effective, saved) in one call with clear, unambiguous semantics, unlike setuid() or setreuid() whose behavior depends on the caller’s current EUID.
getresuid() to verify.
