Preface
In the previous article, we explored runc’s defense-in-depth security model from SPEC.md, but left a core question unanswered: Why must CapPrm and CapEff be separate if they’re both just collections of capabilities? And what’s the difference between CapBnd and them?
To truly answer these questions, we first need to understand two prerequisite concepts: fork and exec. This article takes a zero-to-hero approach, driven by hands-on experiments, to demystify all five attributes of Linux Capabilities.
I. Prerequisites: fork and exec
1.1 fork = Clone a Process
fork() is a Linux system call that creates an identical copy of the current process:
int pid = fork();
if (pid == 0) {
// I am the child process
printf("Child: PID=%d\n", getpid());
} else {
// I am the parent process
printf("Parent: child PID=%d\n", pid);
}Execution result:
Parent: child PID=101
Child: PID=101After fork, the parent and child processes are completely independent — they run separately and don’t interfere with each other.
1.2 exec = Replace the Soul
execve() is another system call that replaces the current process’s memory space with a new program:
// Currently a bash process
execve("/bin/ls", argv, envp);
// After this line, the process PID is still the same (e.g., 100),
// but it has become the ls programKey point: The PID stays the same, but the running program changes.
1.3 fork + exec = Run a New Program
This is the classic Linux process creation pattern:
shell (PID=100)
fork() → child process (PID=101, a clone of shell)
execve("/bin/runc", ...) → PID=101 is now runc
runc starts runningDocker/container process creation follows this pattern: fork first, then exec.
II. The Five Attributes of Capabilities
A Linux process’s capabilities consist of five sets, viewable via cat /proc/self/status:
CapInh: 0000000000000000 # Inheritable
CapPrm: 000001ffffffffff # Permitted
CapEff: 000001ffffffffff # Effective
CapBnd: 000001ffffffffff # Bounding
CapAmb: 0000000000000000 # Ambient2.1 CapEff (Effective) — Currently Active Capabilities
Capabilities the process is actively using right now.
Analogy: Tools in your hand.
2.2 CapPrm (Permitted) — Allowed Capabilities
The upper limit of capabilities a process may ever have.
Analogy: Tools in your bag. You don’t have to use them, but you have them.
2.3 CapBnd (Bounding) — System-Wide Ceiling
A system-level restriction defining what capabilities a process may ever have during its lifetime.
Analogy: Your highest education degree. You can go lower, never higher; once dropped, it cannot be recovered.
2.4 CapInh (Inheritable) — Inherited on fork
Which capabilities child processes inherit from the parent via fork().
Analogy: Genetic inheritance — child processes automatically receive it.
2.5 CapAmb (Ambient) — Preserved After exec
Capabilities that survive an execve() program replacement (only applies to non-root programs).
Analogy: A随身携带的证件 — still valid after swapping programs.
III. Why Must CapPrm and CapEff Be Separate?
This is the most elegant design decision in Capabilities — the Principle of Least Privilege.
3.1 Scenario 1: Web Server
An nginx process needs cap_net_bind_service to bind port 80, but 99% of the time it’s just handling normal HTTP requests and doesn’t need this capability at all.
At startup:
CapPrm = cap_net_bind_service, cap_setuid, cap_sys_admin ...
CapEff = cap_net_bind_service ← briefly needed to bind port 80
After bind() completes:
CapEff = empty ← done, drop it
Processing 10,000 HTTP requests:
CapEff = empty ← normal operations don't need any privilege
When CGI script execution is needed:
CapEff = cap_setuid ← temporarily enabled
Execution completes
CapEff = empty ← drop again
... repeat ...Benefit: If an attacker compromises nginx, they only get whatever CapEff was at that moment. If CapEff were always equal to CapPrm (all capabilities), the attacker immediately has full power.
3.2 Scenario 2: setuid Program Privilege Drop
The sudo process lifecycle:
If CapEff and CapPrm were locked together, temporary privilege escalation would be very cumbersome.
3.3 Scenario 3: Fork Child Process Inheritance
Parent process:
CapInh = {cap_net_bind}
CapPrm = {cap_net_bind}
CapEff = {cap_net_bind}
After fork():
Child automatically inherits:
CapInh = {cap_net_bind}
CapPrm = {cap_net_bind} (inherited from parent)
CapEff = {cap_net_bind}
Child immediately drops CapEff:
CapPrm = {cap_net_bind} (still in the bag)
CapEff = empty
→ If child process is exploited by an attacker, they get CapEff = empty
instead of CapPrm = {cap_net_bind}3.4 One-Line Summary
CapPrm is the "ammunition depot", CapEff is the "gun in hand". The depot can be stolen, but without the gun in hand nothing can be fired. Separation ensures CapEff stays minimal or empty at all times — even when attacked, only minimal privileges are exposed.
IV. CapBnd vs CapPrm
These two are the most commonly confused:
| CapBnd | CapPrm | |
|---|---|---|
| Scope | System-level, global restriction on entire process tree | Process-level, individual process restriction |
| Recovery | No, dropped capabilities are gone forever, even after exec | Yes, capset() can re-enable anytime |
| Initial value | Kernel’s cap_last_cap defines maximum (usually all 40) | Inherited from parent on fork/exec |
4.1 Analogy
CapBnd = Your "highest education degree" (PhD)
→ The maximum you've ever achieved, cannot go higher
→ Even if you drop it, you can never get it back (unless reboot)
CapPrm = "Certificates in your bag" (engineer license + teacher license + ...)
→ Can discard anytime, can earn new ones to replace4.2 CapBnd’s Real Value: Preventing Privilege Escalation
CapBnd is the last line of defense against privilege escalation by already-capable processes.
Docker container escape scenario:
Host runc process:
CapBnd = full (all 40 capabilities)
CapPrm = full
CapEff = full
When creating a container, runc trims the container's CapBnd (removing cap_sys_admin, etc.):
Container process:
CapBnd = {without CAP_SYS_ADMIN} ← kernel-enforced!
CapPrm = {without CAP_SYS_ADMIN}
CapEff = {without CAP_SYS_ADMIN}
→ Even if container process escalates via vulnerability, it cannot
execute mount()/unshare()/chroot()
because these require CAP_SYS_ADMIN which is not in CapBndCapBnd is the iron gate; CapPrm is the wooden door.
4.3 Operational Comparison
CapBnd can only be dropped, never recovered:
capsh --drop=cap_sys_admin --
capsh --add=cap_sys_admin --printCapPrm can be dropped and recovered:
capsh --drop=cap_net_bind_service --
capsh --add=cap_net_bind_service --V. CapInh vs CapAmb
| CapInh | CapAmb | |
|---|---|---|
| Trigger | fork inheritance | exec preservation |
| Purpose | Controls what children get | Controls what survives exec |
| Typical use | Forking privileged children | Maintaining privilege across exec |
5.1 How CapInh Works
Parent process:
CapInh = {cap_net_bind_service}
fork()
↓ child inherits
Child CapInh = {cap_net_bind_service} (automatic)
Child CapPrm = {cap_net_bind_service} (inherited from CapInh)5.2 How CapAmb Works
Normal programs lose all capabilities after exec:
Normal process (no root):
execve("/bin/ls", ...)
→ Loses all capabilities (because ls is not setuid)CapAmb allows some to survive exec:
Normal process:
CapPrm = {cap_net_bind_service}
CapEff = {cap_net_bind_service}
CapAmb = {cap_net_bind_service} ← marked "preserve after exec"
execve("/bin/myprogram", ...)
→ myprogram still has cap_net_bind_service5.3 CapAmb’s Three Restrictions
- The program must not be setuid/setgid
CapAmb ≤ CapPrm(cannot preserve what isn’t in the bag)CapAmb ≤ CapInh(cannot preserve what isn’t marked inheritable)
VI. Real-World Cases of CapInh and CapAmb
6.1 CapInh Case: Nginx Forking Worker Processes
nginx master process (root):
CapInh = {cap_net_bind_service} ← marked for inheritance
fork()
↓ child inherits
nginx worker process:
CapPrm = {cap_net_bind_service} (inherited from CapInh)
CapEff = {cap_net_bind_service}6.2 CapAmb Case: Normal User Running Network Service
Normal user process:
CapAmb = {cap_net_bind_service}
execve("/usr/sbin/my-server", ...)
→ my-server can still bind port 80 (even without root)VII. Full Initialization Sequence
Process startup (exec):
CapBnd = kernel's cap_last_cap determines maximum set (usually all 40)
↓
CapPrm = inherited from parent
↓
CapEff = inherited from parent
↓
CapInh = inherited from parent (can be explicitly modified)
↓
CapAmb = inherited from parent (can be explicitly modified)VIII. Common Commands for Viewing and Manipulating Capabilities
8.1 View current process capabilities
cat /proc/self/status | grep CapExample output:
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 00000000000000008.2 Decode hexadecimal to readable list
sudo apt install libcap-ng-utils
capsh --decode=000001ffffffffffExample output:
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,
cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,
cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,
cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,
cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,
cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,
cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap ...8.3 capsh tool reference
capsh --print
capsh --drop=cap_sys_admin --print
capsh --add=cap_net_bind_service --print
capsh --caps="cap_net_bind_service+ep" -- -c "..."8.4 View any process’s capabilities
cat /proc/1234/status | grep Cap
capsh --decode=$(cat /proc/1234/status | grep CapEff | awk '{print $2}')8.5 getpcaps tool (view other processes)
sudo apt install libcap-ng-utils
getpcaps 1234IX. Hands-On Experiments
9.1 Experiment 1: Compare root vs normal user capabilities
sudo cat /proc/$/status | grep Cap
su - testuser -c 'cat /proc/self/status | grep Cap'Compare: root’s CapEff is usually full (000001ffffffffff), while normal users typically have only a subset.
9.2 Experiment 2: CapBnd cannot be recovered after drop
capsh --drop=cap_sys_admin --
capsh --add=cap_sys_admin --print9.3 Experiment 3: CapInh inheritance across fork
In an environment with root privileges:
// fork_test.c
#include <stdio.h>
#include <unistd.h>
#include <sys/capability.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("Child: CapEff=%016llx\n", (unsigned long long)cap_get_flag(cap_get_proc(), CAP_EFFECTIVE));
_exit(0);
} else {
wait(NULL);
}
return 0;
}9.4 Experiment 4: CapAmb preserved after exec
As a normal user (non-root):
sudo setcap cap_net_bind_service=ep /usr/sbin/python3
su - testuser -c "capsh --caps='cap_net_bind_service=ep' --addamb=cap_net_bind_service -- -c 'getpcaps \$\$'"9.5 Experiment 5: Observe Docker container’s CapBnd
docker run --rm ubuntu cat /proc/self/status | grep Cap
cat /proc/self/status | grep CapContainer CapBnd typically has many fewer capabilities than the host (e.g., no CAP_SYS_ADMIN).
X. Capabilities in runc
When runc creates a container, it sets the container process’s capabilities based on the linux.capabilities field in config.json:
// libcontainer/capabilities/capabilities.go
func (c *linuxCap) ApplyCaps() error {
// 1. Drop all capabilities
for _, cap := range capability.List() {
cap.Set(cap.Effective, false)
cap.Set(cap.Permitted, false)
cap.Set(cap.Inheritable, false)
}
// 2. Enable only capabilities declared in config.json
for _, cap := range c.bounds {
cap.Set(cap.Permitted, true)
cap.Set(cap.Effective, true)
}
return nil
}The container’s CapBnd is also trimmed — removing dangerous capabilities the container doesn’t need (like CAP_SYS_ADMIN, CAP_SYS_MODULE, etc.) to prevent container escape via these capabilities.
XI. Seccomp Deep Dive: Capabilities’ Companion
11.1 What is Seccomp
Seccomp (Secure Computing Mode) is a Linux kernel system call filter that restricts which system calls (syscalls) a process can invoke.
Every program interacts with the kernel via syscalls:
User program ──read()──→ kernel
User program ──write()──→ kernel
User program ──mount()──→ kernel ← dangerous!
User program ──unshare()──→ kernel ← dangerous!Seccomp’s role is to intercept syscalls before they reach the kernel handler:
Without Seccomp:
Program ──mount──→ ✅ Passes through ← dangerous!
With Seccomp (only allows read/write/exit):
Program ──mount──→ ❌ Rejected (returns -1, errno = EPERM)11.2 Two Seccomp Modes
Mode 1 (Strict Mode, since kernel 2.6.12)
All-or-nothing, inflexible:
echo 1 > /proc/self/seccompMode 2 (Filter Mode, since kernel 3.5)
Allows rule-based syscall filtering — this is the mainstream approach:
Allowed: read, write, exit, sigreturn
Blocked: mount, unshare, mknod, chroot, ...11.3 The Relationship Between Capabilities and Seccomp
They do overlap functionally, but have different design goals:
| Capabilities | Seccomp | |
|---|---|---|
| Granularity | Individual capability | Individual system call |
| Layer | Linux userspace permission model | Kernel-enforced |
| Relationship | "Do you have this license?" | "Are you allowed to call this syscall?" |
Key difference:
Capabilities check happens INSIDE the syscall handler (allowed if you have the license)
Seccomp filter intercepts BEFORE the syscall reaches the handlerBoth together provide double insurance:
Attacker exploits vulnerability and gains CAP_SYS_ADMIN:
With Capabilities only:
mount() ──→ has CAP_SYS_ADMIN ──→ Success ⚠️ Container escape!
With Capabilities + Seccomp:
mount() ──→ Seccomp intercepts ──→ EPERM ❌
Even with CAP_SYS_ADMIN, it doesn't workAnalogy:
Capabilities = Security guard (checks your pass)
Seccomp = Door lock (physically prevents opening)11.4 Viewing and Configuring Seccomp
Check process Seccomp status:
cat /proc/self/status | grep SeccompEnable Seccomp (Mode 2):
#include <linux/seccomp.h>
#include <sys/prctl.h>
// Enable Seccomp filter mode
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);Write rules with libseccomp:
#include <seccomp.h>
int main() {
// Create filter: allow all by default
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
// Block mount
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM),
SCMP_SYS(mount), 0);
// Block unshare
seccomp_rule_add(ctx, SCMP_ACT_ERRNO(EPERM),
SCMP_SYS(unshare), 0);
// Load rules
seccomp_load(ctx);
// Now mount() is rejected
mount("none", "/tmp", "tmpfs", 0, NULL); // Returns EPERM
}Verify Seccomp interception:
strace mount -t tmpfs none /tmp 2>&1Seccomp in runc (config.json):
{
"linux": {
"seccomp": {
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["mount", "umount2", "unshare", "chroot", "mknod"],
"action": "SCMP_ACT_ERRNO"
}
]
}
}
}XII. How to Configure Capability for a Process
12.1 Check Process Capability
cat /proc/self/status | grep Cap
getpcaps <pid>12.2 Set Capability on Executable File (Permanent)
Use setcap to set capability on a binary file:
sudo setcap cap_net_bind_service=ep /usr/bin/python3
sudo setcap cap_net_raw=ep /bin/ping
sudo setcap cap_net_bind_service,cap_net_admin=ep /usr/sbin/myapp
getcap /usr/bin/python3
sudo setcap -r /usr/bin/python312.3 Adjust Capability on Running Process (Temporary)
Use capsh:
capsh --drop=cap_sys_admin --print
capsh --caps="cap_net_bind_service+ep" -- -c "python3 -m http.server 80"
capsh --add=cap_net_bind_service --12.4 Set Capability in Code (libcap)
#include <sys/capability.h>
int main() {
// Get current process capabilities
cap_t caps = cap_get_proc();
// Clear all capabilities
cap_clear(caps);
// Add cap_net_bind_service to Effective and Permitted
cap_value_t vals[] = {CAP_NET_BIND_SERVICE};
cap_set_flag(caps, CAP_EFFECTIVE, 1, vals, CAP_SET);
cap_set_flag(caps, CAP_PERMITTED, 1, vals, CAP_SET);
// Apply to current process
cap_set_proc(caps);
// Now can bind port 80
bind(sock, ...);
cap_free(caps);
return 0;
}Compile:
gcc -o myapp myapp.c -lcap12.5 Configure in Docker
docker run --rm --cap-add=SYS_ADMIN ubuntu bash
docker run --rm --cap-add=NET_ADMIN ubuntu bash
docker run --rm --cap-drop=SYS_ADMIN ubuntu bash
docker run --rm \
--cap-add=SYS_ADMIN \
--cap-add=NET_ADMIN \
--cap-drop=ALL \
ubuntu bash
docker run --rm --privileged ubuntu bash12.6 Configure in runc config.json
{
"linux": {
"capabilities": {
"bounding": ["CAP_NET_BIND_SERVICE", "CAP_CHOWN"],
"effective": ["CAP_NET_BIND_SERVICE", "CAP_CHOWN"],
"permitted": ["CAP_NET_BIND_SERVICE", "CAP_CHOWN"],
"inheritable": ["CAP_NET_BIND_SERVICE"],
"ambient": ["CAP_NET_BIND_SERVICE"]
}
}
}12.7 Configure in Kubernetes
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: mycontainer
image: ubuntu
securityContext:
capabilities:
add:
- SYS_ADMIN
- NET_ADMIN
drop:
- ALL12.8 Summary: Capability Configuration Methods
Permanent (file-level):
setcap cap_xxx+ep /path/to/binary
↓
File has extended attributes (xattr)
↓
exec() loads into process automatically
Temporary (process-level):
capsh / cap_set_proc()
↓
Affects only current process and children
↓
Gone when process exits
Container-level:
docker run --cap-add=xxx
↓
Rewrites container's CapBnd/CapPrm
↓
runc applies via libcontainerXIII. Summary
| Attribute | Meaning | Scope | Recoverable |
|---|---|---|---|
| CapEff | Currently active capabilities | Process-level | Yes, via capset() anytime |
| CapPrm | Allowed capabilities (upper bound) | Process-level | Yes, via capset() anytime |
| CapBnd | Global ceiling (Bounding) | System-level | No |
| CapInh | Inherited on fork | Process-level | Yes, modifiable anytime |
| CapAmb | Preserved after exec | Process-level | Yes, via capset() anytime |
Core design philosophy: Principle of Least Privilege. CapEff should always remain minimal — used and dropped immediately. CapBnd is the last line of defense against privilege escalation.
This is the sixth article in the runc source code analysis series, focusing on Linux Capabilities design philosophy and experimental verification.