Skip to content
/linux-syscalls

Security & Credentials · Section 2

setuid(2)

Set the calling process's user ID — the canonical privilege-dropping primitive.

Signature

#include <unistd.h>

int setuid(uid_t uid);
uid
Target user ID. 0 = root; UIDs > 0 are unprivileged unless granted specific capabilities.

Description

setuid() sets the effective user ID of the calling process. If the caller is privileged (has CAP_SETUID), it also sets the real UID and saved set-user-ID — an irreversible drop from root to an unprivileged identity. If the caller is unprivileged, setuid() can only set the EUID to one of {RUID, EUID, SUID} — the limited POSIX guarantee that lets set-UID binaries toggle between their two identities. setuid(0) called by a non-root process always fails. The typical privileged-daemon pattern: open privileged resources (bind port < 1024, read /etc/shadow, drop CAP_*), then setuid() to a dedicated user — releasing root for the rest of the process's lifetime. Note: setuid() does NOT change capabilities directly; use prctl(PR_SET_KEEPCAPS) before setuid() if you need to retain a capability across the UID change, or use capset() after.

Architecture mapping

ArchitectureNumberABIEntry point
x86 (i386)23i386sys_setuid16
x64 (x86_64)105commonsys_setuid
ARM64 (aarch64)146sys_setuid

Kernel history

Introduced in Linux 1.0.

  1. 1.0

    setuid() has been part of Linux since 1.0 with POSIX semantics. The i386 entry point sys_setuid16 reflects the original 16-bit UID width; the 32-bit variant is reached via a different syscall number on legacy code paths.

  2. 2.4

    32-bit UIDs were introduced; legacy 16-bit syscalls (sys_setuid16) remained for ABI compatibility but new code uses the 32-bit variants.

  3. 3.8

    User namespaces (CLONE_NEWUSER) added a per-namespace UID mapping. setuid() in an unprivileged userns operates within the namespace's mapping — the caller can be 'root' inside the userns while being a normal user on the host. This is the foundation of rootless containers but also a CVE-rich area.

seccomp & containers

Docker default profile

Allowed

Podman default profile

Allowed

setuid() and the rest of the UID/GID family are on Docker / Podman default profiles because container init processes use them to drop privileges before spawning workers. Workloads that don't need privilege changes (every container that runs as a single fixed user) can deny the entire family — a useful hardening that closes the 'compromised root child escalates to a different identity' route. The libseccomp recipe above blocks all eight relevant variants.

libseccomp

// Block uid manipulation if the workload never needs to drop privileges
for (int s : { SCMP_SYS(setuid), SCMP_SYS(setreuid), SCMP_SYS(setresuid),
               SCMP_SYS(setgid), SCMP_SYS(setregid), SCMP_SYS(setresgid),
               SCMP_SYS(setfsuid), SCMP_SYS(setfsgid) })
    seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM), s, 0);

strace example

$ strace -e setuid,setresuid sudo -u nobody true 2>&1 | head -3
setresuid(65534, 65534, 65534)          = 0
setuid(65534)                           = 0

Modern code typically reaches setresuid() rather than setuid() (because it's safer and more explicit). strace shows both. A daemon's startup trace should show setuid() / setgid() / capset() exactly once each, very early; later calls are unusual.

Security & observability

setuid() is the focal point of privilege management — auditing which processes ever call it and to which UIDs is the bedrock of any host hardening. Set-UID-root binaries are the classic privilege-escalation surface (CVE-2021-3156 sudo, CVE-2023-22809 sudoedit, etc.). Within a process, common security failures include: forgetting to call setgid() before setuid() (the saved set-group-ID is lost if you setuid() first), not checking the return value (setuid() can fail with EAGAIN under load — many daemons would happily keep running as root), and assuming setuid() drops capabilities (it doesn't — use prctl or capset). eBPF tracepoint sys_enter_setuid captures every call; alerting on setuid(0) from a non-init container PID is a strong signal.

Errors

EAGAIN
uid does not match the calling thread's current UID and RLIMIT_NPROC for the target user has been reached.
EINVAL
uid is not valid in the caller's user namespace (e.g. unmapped UID in an unprivileged userns).
EPERM
Caller lacks CAP_SETUID and uid is not one of the existing RUID/EUID/SUID.

Related syscalls