The capabilities system must coexist with the traditional UNIX user ID (UID) model. When a process changes its user IDs β for example, a set-user-ID-root program calling setuid() to drop root β the kernel automatically adjusts the process’s capability sets to maintain compatibility with traditional security semantics.
These automatic adjustments mean that transitions between UID 0 and non-zero UIDs have predictable, well-defined effects on a process’s capabilities. Understanding these rules is critical for writing security-correct daemons and privileged programs.
The kernel enforces the following rules whenever a process changes its user IDs. These rules are applied automatically by the kernel β the process does not need to take any action.
If the real UID, effective UID, OR saved set-user-ID previously had the value 0 β and as a result of the UID change, all three of these IDs become nonzero β then both the permitted and effective capability sets are cleared completely. All capabilities are permanently dropped.
What this means in practice: A process that completely gives up root (all three UIDs become non-zero) loses all capabilities permanently. This is the “nuclear option” β full privilege dropping. It is analogous to a set-user-ID-root program calling setuid(non_zero) such that the saved set-user-ID also becomes non-zero.
calls setresuid(500, 500, 500)
β permitted = 0 (empty), effective = 0 (empty)
β All capabilities PERMANENTLY gone
If the effective UID changes from 0 to a nonzero value, then the effective capability set is cleared (dropped to empty). However, the permitted capability set is unchanged β the process still has those capabilities in permitted and can raise them back to effective later if it returns effective UID to 0.
What this means in practice: A root process that temporarily drops effective UID (like a traditional set-user-ID program temporarily dropping privileges) also loses its effective capabilities. This maintains consistency: if the process is no longer effectively root, it shouldn’t have root-level capabilities active either.
calls seteuid(500)
β effective = 0 (empty)
β permitted = unchanged {CAP_SYS_TIME, CAP_NET_RAW}
If the effective UID changes from a nonzero value to 0 (gaining root), then the permitted capability set is copied into the effective capability set. All capabilities that were in the permitted set become immediately active.
What this means in practice: A process that regains root effective UID (e.g., a set-user-ID-root program calling seteuid(0) to restore privileges) automatically regains all capabilities that were in its permitted set. This mirrors the traditional semantics where becoming effectively root means having all privileges.
calls seteuid(0)
β effective = copies permitted = {CAP_SYS_TIME}
Linux has a special file-system user ID (fsuid) used for file permission checks. When the file-system UID changes between 0 and nonzero, a specific subset of file-related capabilities is adjusted:
- fsuid transitions 0 β nonzero: These capabilities are cleared from the effective set:
CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_DAC_READ_SEARCH,CAP_FOWNER,CAP_FSETID,CAP_LINUX_IMMUTABLE,CAP_MAC_OVERRIDE,CAP_MKNOD - fsuid transitions nonzero β 0: Any of these same capabilities that are in the permitted set are also re-enabled in the effective set.
This rule maintains the traditional semantics for the Linux-specific setfsuid()/setfsgid() operations, which change only the file-system UID/GID without affecting other UIDs.
| UID Transition | Effect on Effective Set | Effect on Permitted Set |
|---|---|---|
| All 3 UIDs: some-zero β all-nonzero | Cleared (all caps dropped) | Cleared (all caps dropped) |
| EUID: 0 β nonzero | Cleared | Unchanged |
| EUID: nonzero β 0 | = Permitted (copied) | Unchanged |
| fsuid: 0 β nonzero | File-related caps cleared | Unchanged |
This program demonstrates all three main UID-to-capability transition rules. It must be run as root (or as a set-user-ID-root binary) to show the transitions. It prints the capability state before and after each UID change to make the kernel’s automatic adjustments visible.
/*
* uid_cap_transitions.c
*
* Demonstrates how the kernel automatically adjusts capability sets
* when a process changes its user IDs. Shows all three main rules:
*
* Rule 2: EUID 0 -> nonzero: effective caps cleared
* Rule 3: EUID nonzero -> 0: permitted copied to effective
* Rule 1: All UIDs -> nonzero: both permitted and effective cleared
*
* MUST run as root (or as set-user-ID-root):
* sudo ./uid_cap_transitions
* OR:
* sudo chown root ./uid_cap_transitions
* sudo chmod u+s ./uid_cap_transitions
* ./uid_cap_transitions
*
* Compile:
* gcc -o uid_cap_transitions uid_cap_transitions.c -lcap
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/capability.h>
#include <unistd.h>
/*
* print_cap_summary() - Print a one-line summary of the process's capability state.
* We show the compact text representation plus the current UIDs.
*/
static void print_cap_summary(const char *label)
{
cap_t caps;
char *text;
caps = cap_get_proc();
if (caps == NULL) { perror("cap_get_proc"); return; }
text = cap_to_text(caps, NULL);
printf(" [%-40s] RUID=%-4d EUID=%-4d caps=[%s]\n",
label,
(int)getuid(), (int)geteuid(),
(text && text[0]) ? text : "none");
if (text) cap_free(text);
cap_free(caps);
}
int main(void)
{
printf("=== UID to Capability Transition Demo ===\n\n");
if (geteuid() != 0) {
fprintf(stderr,
"This program must run with effective UID 0 (root).\n"
"Try: sudo ./uid_cap_transitions\n");
return 1;
}
/* ================================================================
* Initial state: running as root
* Expected: caps = "=ep" (all capabilities in effective + permitted)
* ================================================================ */
printf("--- Initial state (root) ---\n");
print_cap_summary("Initial: EUID=0");
/* ================================================================
* RULE 2 DEMONSTRATION
* Change effective UID from 0 to non-zero.
* Expected effect: effective capability set is CLEARED.
* Permitted set is preserved.
* ================================================================ */
printf("\n--- Demonstrating Rule 2: EUID 0 -> nonzero ---\n");
printf(" (Effective caps should be CLEARED; permitted unchanged)\n");
/*
* seteuid() changes only the effective UID.
* The real UID and saved set-user-ID remain 0.
* Per Rule 2: effective caps are cleared.
*/
if (seteuid(1000) == -1) { perror("seteuid(1000)"); return 1; }
print_cap_summary("After seteuid(1000)");
/* ================================================================
* RULE 3 DEMONSTRATION
* Change effective UID back from non-zero to 0.
* Expected effect: permitted capability set is COPIED to effective.
* ================================================================ */
printf("\n--- Demonstrating Rule 3: EUID nonzero -> 0 ---\n");
printf(" (Permitted caps should be COPIED to effective)\n");
/*
* seteuid(0) is possible here because the saved set-user-ID is still 0.
* Per Rule 3: effective = permitted (all caps restored to effective).
*/
if (seteuid(0) == -1) { perror("seteuid(0)"); return 1; }
print_cap_summary("After seteuid(0) again");
/* ================================================================
* RULE 1 DEMONSTRATION
* Change ALL user IDs to non-zero.
* Expected effect: BOTH permitted AND effective capability sets cleared.
* This is IRREVERSIBLE β cannot regain capabilities after this!
* ================================================================ */
printf("\n--- Demonstrating Rule 1: ALL UIDs -> nonzero ---\n");
printf(" (BOTH permitted AND effective caps should be CLEARED)\n");
printf(" (THIS IS IRREVERSIBLE)\n");
/*
* setresuid(real, effective, saved) sets all three UIDs at once.
* When all three become nonzero:
* - Permitted capabilities: CLEARED
* - Effective capabilities: CLEARED
* - This cannot be undone β the process has permanently dropped all caps.
*/
if (setresuid(1000, 1000, 1000) == -1) {
perror("setresuid(1000,1000,1000)");
return 1;
}
print_cap_summary("After setresuid(1000,1000,1000)");
/* Attempting to regain root would fail because saved set-user-ID is now 1000 */
printf("\n--- Attempting seteuid(0) now (should fail) ---\n");
if (seteuid(0) == -1) {
printf(" seteuid(0) FAILED as expected: %m\n");
printf(" (All UIDs are nonzero; saved set-user-ID is no longer 0)\n");
} else {
printf(" seteuid(0) unexpectedly succeeded\n");
}
print_cap_summary("Final state");
printf("\nProcess has no capabilities and cannot regain them.\n");
return 0;
}
--- Initial state (root) --- [Initial: EUID=0 ] RUID=0 EUID=0 caps=[=ep] --- Demonstrating Rule 2: EUID 0 -> nonzero --- [After seteuid(1000) ] RUID=0 EUID=1000 caps=[=p] --- Demonstrating Rule 3: EUID nonzero -> 0 --- [After seteuid(0) again ] RUID=0 EUID=0 caps=[=ep] --- Demonstrating Rule 1: ALL UIDs -> nonzero --- [After setresuid(1000,1000,1000) ] RUID=1000 EUID=1000 caps=[none] seteuid(0) FAILED as expected: Operation not permitted
This sets all three user IDs (real, effective, saved set-user-ID) to 500, meaning all go from 0 to non-zero. This triggers Rule 1: both the permitted and effective capability sets are completely cleared. The process permanently loses all capabilities and cannot regain them, because the saved set-user-ID is now 500 (so it cannot call seteuid(0) to restore root).
Rule 2 applies: the effective UID changes from 0 to 1000 (non-zero). The kernel automatically clears the effective capability set β the process loses all active capabilities. However, the permitted capability set is unchanged. Because the real UID and saved set-user-ID are still 0, the daemon can later call seteuid(0) to restore root effective UID, at which point Rule 3 applies: the permitted set is copied back to the effective set, restoring all active capabilities.
When only EUID changes 0βnonzero (Rule 2): only the effective capability set is cleared. The permitted set is preserved, so the process can regain its effective capabilities by returning EUID to 0. When all UIDs go to nonzero (Rule 1): BOTH the effective AND the permitted capability sets are cleared. This is permanent and irreversible β the process has truly surrendered all privileges.
When the file-system UID (fsuid) changes from 0 to non-zero, these file-related capabilities are cleared from the effective set: CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_DAC_READ_SEARCH, CAP_FOWNER, CAP_FSETID, CAP_LINUX_IMMUTABLE, CAP_MAC_OVERRIDE, and CAP_MKNOD. When fsuid changes from non-zero to 0, any of these that are in the permitted set are re-enabled in the effective set.
The automatic adjustments are necessary for backward compatibility with the enormous body of existing privileged UNIX software. These programs were written under the assumption that changing user IDs would directly control their level of privilege. Without automatic adjustments, a program that calls setuid() to drop root would still retain its capabilities β gaining no security benefit and violating user expectations. The automatic rules ensure that the capability system integrates transparently with the UID-based model.
β Part 4: exec() Transformation Next: Changing Capabilities Programmatically β
