Operate with Least Privilege โ€” Temporarily and Permanently Dropping Privileges

 

Operate with Least Privilege
Chapter 38.2 โ€” Temporarily and Permanently Dropping Privileges
๐Ÿ” seteuid()
๐Ÿ›ก๏ธ setuid()
โš™๏ธ setresuid()

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.

๐Ÿง  Understanding the Saved Set-UID Mechanism

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 privilege
seteuid(getuid())
1000 1000 0 Unprivileged work. Saved UID=0 preserved for later.
Re-raise privilege
seteuid(orig_euid)
1000 0 0 Back to root for privileged work
Permanent drop
setuid(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.

โฑ๏ธ Temporarily Dropping and Re-Acquiring Privilege

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;
}

๐Ÿ”’ Permanently Dropping Privilege

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);
}

โœ… Always Verify After Changing Credentials

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;
}

๐Ÿ‘ฅ Don’t Forget Group IDs

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

Least Privilege seteuid() setuid() setreuid() setresuid() setegid() setresgid() getresuid() Temporary Drop Permanent Drop Saved Set-UID Stack Crashing

๐ŸŽฏ Interview Questions โ€” Least Privilege
Q1. What is the principle of least privilege and why is it critical for SUID programs?A program should hold only the permissions it currently needs. For SUID programs, any bug in code running with root privilege becomes a security hole. Dropping privilege when not needed limits the window of vulnerability.

Q2. What is the difference between seteuid() and setuid() in terms of dropping privilege?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.

Q3. Why is the sequence “seteuid(getuid()) then setuid(getuid())” wrong for permanent privilege drop?After 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.

Q4. Which system call is preferred for permanently dropping privilege and why?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.

Q5. Why should you verify credential changes after calling setuid/seteuid?These calls can fail or partially succeed (e.g., if capabilities have been manipulated, or on some non-Linux systems). A silent failure means the program believes it dropped privilege but is still running as root โ€” a critical security failure. Always follow with getresuid() to verify.

Q6. When dropping multiple credential types (UID, GID, supplementary groups), what is the correct order?Supplementary groups first, then group ID, then user ID last. When raising privilege, reverse the order: user ID first. This is because some operations (like modifying supplementary groups) require root EUID, which would be lost if user ID was dropped first.

Next: Safe Program Execution โ†’
Dropping privilege before exec(), avoiding shells, closing file descriptors

Continue to Part 3 โ† Part 1

Leave a Reply

Your email address will not be published. Required fields are marked *