The Soft/Hard Limit System
Every resource has two limits that work together. The soft limit is what the kernel actually enforces — it is the wall a process hits when consuming the resource. The hard limit is a governance ceiling on how high the soft limit can be raised. Think of the soft limit as the current speed limit and the hard limit as the absolute maximum speed the road is ever allowed to have.
Who Can Change What
| Process Type | Can change rlim_cur (soft) | Can change rlim_max (hard) |
|---|---|---|
| Unprivileged process | ✓ Can set to any value from 0 up to rlim_max | ✗ Can only lower it (irreversibly). Cannot raise. |
| Privileged (CAP_SYS_RESOURCE) | ✓ Can set to any value including above rlim_max | ✓ Can raise or lower as long as rlim_max ≥ rlim_cur |
Inheritance — fork() and exec()
Resource limits are inherited by child processes created with fork(), and they are preserved across exec(). This is how the shell’s ulimit settings cascade to all programs the user runs.
The child process starts with an exact copy of the parent’s resource limits. If parent had RLIMIT_NOFILE = 1024/4096 (soft/hard), the child starts with the same values.
When a process calls exec() to replace itself with a new program, the resource limits are preserved. The new program starts with the same limits the old program had.
/* Common pattern: sandbox an untrusted program */
pid_t pid = fork();
if (pid == 0) {
struct rlimit rl;
/* Set tight limits on the child BEFORE exec */
rl.rlim_cur = rl.rlim_max = 30; /* max 30 procs */
setrlimit(RLIMIT_NPROC, &rl);
rl.rlim_cur = rl.rlim_max = 64*1024*1024; /* 64 MB memory */
setrlimit(RLIMIT_AS, &rl);
rl.rlim_cur = rl.rlim_max = 10; /* 10 sec CPU */
setrlimit(RLIMIT_CPU, &rl);
/* exec() inherits these limits — untrusted code is caged */
execvp(argv[1], &argv[1]);
perror("execvp"); exit(1);
}
Per-Process vs Per-User-ID Measurement
Most resource limits are measured against the process itself only. But some limits — notably RLIMIT_NPROC and RLIMIT_SIGPENDING — are measured against all processes with the same real user ID.
The reason for the user-ID scope becomes clear with RLIMIT_NPROC. If the limit only applied to the process’s own children, a process could trivially bypass it by having each child spawn more children. So instead, the limit counts all processes owned by that user ID — making it a genuine ceiling on total process creation by a user.
Table 36-1 — All Resource Constants
Code Example — Demonstrating Limit Rules
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/resource.h>
int main(void)
{
struct rlimit rl;
getrlimit(RLIMIT_STACK, &rl);
printf("Stack soft: %lld bytes\n", (long long)rl.rlim_cur);
printf("Stack hard: %lld bytes\n", (long long)rl.rlim_max);
/* Unprivileged: try to LOWER the hard limit */
rlim_t original_hard = rl.rlim_max;
rl.rlim_max = rl.rlim_cur; /* lower hard to equal soft */
if (setrlimit(RLIMIT_STACK, &rl) == 0) {
printf("Successfully lowered hard limit to %lld\n",
(long long)rl.rlim_max);
}
/* Now try to RAISE the hard limit back — this will fail */
rl.rlim_max = original_hard;
if (setrlimit(RLIMIT_STACK, &rl) == -1) {
if (errno == EPERM)
printf("EPERM: Cannot raise hard limit — irreversible!\n");
}
/* Unprivileged: try to raise soft ABOVE hard — should fail */
getrlimit(RLIMIT_STACK, &rl);
rl.rlim_cur = rl.rlim_max + 1;
if (setrlimit(RLIMIT_STACK, &rl) == -1) {
if (errno == EINVAL)
printf("EINVAL: Cannot set soft above hard\n");
}
return 0;
}
/* Compile: gcc -o limits_rules limits_rules.c */
Interview Questions
An unprivileged process can only lower its hard limit, and this lowering is irreversible — it cannot be raised again without root/CAP_SYS_RESOURCE privilege. It cannot raise the hard limit at all. Only a privileged process (CAP_SYS_RESOURCE) can raise the hard limit.
Resource limits are inherited by child processes created via fork() — the child gets exact copies of the parent’s limits. They are also preserved across exec() — when a process replaces itself with a new program, the limits remain unchanged. This is how shell ulimit settings cascade to all commands the shell runs.
RLIMIT_NPROC (total processes for that user ID) and RLIMIT_SIGPENDING (queued signals for that user ID). These are per-user-ID because a per-process scope would be trivially bypassable.
In most cases yes — resource limits apply to both privileged and unprivileged processes. The exception is RLIMIT_NPROC: this is not enforced for processes with CAP_SYS_ADMIN or CAP_SYS_RESOURCE. Most other limits still apply even to root.
