no_new_privs and Privilege Escalation Control
no_new_privsis a single, one-way bit on a task that makes the kernel promise: from now on, noexecvecan ever grant this process a privilege it does not already hold. Set it viaprctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)and the kernel disables the privilege-granting side effects ofexecvefor that task and all its descendants — setuid and setgid bits stop changing the effective user/group ID, file capabilities stop adding to the permitted set, and Linux Security Module (LSM) profile transitions stop relaxing constraints. The flag is inherited acrossfork,clone, andexecve, and cannot be unset (kernel no_new_privs doc). It was added in Linux 3.5. This bit is the keystone of unprivileged sandboxing: both unprivileged seccomp filters and Landlock require it precisely because, without it, a sandboxed process could escape byexecing a setuid-root binary that would run with elevated privileges inside the sandbox — a confused, more-privileged child the unprivileged parent had no business constraining.
This note covers PR_SET_NO_NEW_PRIVS specifically — the flag, its enforcement, and its role as a precondition for self-sandboxing. The broader capability-set machinery it interacts with lives in POSIX Capabilities and Capability Transitions Across execve; the sandbox primitives that depend on it are Seccomp and seccomp-BPF and Landlock.
Versions are pinned to the 6.12 long-term-support kernel; every code excerpt below was read from the v6.12 tag of the mainline tree during this write-up. The flag itself has existed since Linux 3.5 (2012) and its user-visible contract has not changed since. As of 2026-09-04, 6.12 is a maintained LTS while mainline is 7.x; the one place a later kernel is relevant is LANDLOCK_RESTRICT_SELF_TSYNC (Landlock ABI 8, mainline 7.0), which propagates no_new_privs across threads the way seccomp’s TSYNC already does — noted and dated where it comes up.
Mental Model — A Gate on One Doorway, Not a Privilege Freeze
Before any mechanism, the contract. The kernel’s own no_new_privs documentation enumerates the guarantee in one sentence and it is worth taking apart word by word: “With no_new_privs set, execve() promises not to grant the privilege to do anything that could not have been done without the execve call. For example, the setuid and setgid bits will no longer change the uid or gid; file capabilities will not add to the permitted set, and LSMs will not relax constraints after execve” (Documentation/userspace-api/no_new_privs.rst, v6.12).
flowchart TB E["execve(prog)"] --> Q{"task_no_new_privs(current)?"} Q -->|"no — ordinary exec"| N1["setuid/setgid bits apply<br/>→ euid/egid raised"] Q -->|"no"| N2["file capabilities apply<br/>→ permitted set raised"] Q -->|"no"| N3["LSM may transition to a<br/>different (possibly wider) profile"] Q -->|yes| B1["1. SETUID / SETGID<br/>bprm_fill_uid() returns early:<br/>euid/egid never raised at all"] Q -->|yes| B2["2. FILE CAPABILITIES<br/>read from disk, then<br/>cap_permitted ∩= old permitted"] Q -->|yes| B3["3. LSM RELAXATION<br/>AppArmor refuses a transition<br/>that is not a subset (-EPERM)"] B1 --> R["Privilege ceiling after exec<br/>≤ ceiling before exec"] B2 --> R B3 --> R N1 --> R2["Ceiling may RISE"] N2 --> R2 N3 --> R2 R -.->|"NOT blocked"| X["setuid(2) by an already-privileged task<br/>SCM_RIGHTS fd passing<br/>anything not going through execve"]
The three escalation channels execve normally offers, and what the bit does to each. What it shows: the flag is not a single check but three separate interventions at three different places in the exec path — one in fs/exec.c that suppresses setuid/setgid before the credentials are even proposed, one in security/commoncap.c that lets file capabilities be computed and then intersects them away, and one delegated to whichever LSM is active. The insight to take: the guarantee is scoped precisely to execve, and the dashed branch is the part people forget — “no_new_privs does not prevent privilege changes that do not involve execve(). An appropriately privileged task can still call setuid(2) and receive SCM_RIGHTS datagrams.” It is a gate on one doorway, not a general privilege freeze.
| Channel | Without no_new_privs | With no_new_privs | Enforced where |
|---|---|---|---|
| set-user-ID / set-group-ID mode bits | euid/egid become the file’s owner/group | Ignored entirely — never proposed | bprm_fill_uid(), fs/exec.c |
File capabilities (security.capability xattr) | Raise the new permitted set | Computed, then intersected with the old permitted set | cap_bprm_creds_from_file(), security/commoncap.c |
| LSM profile transition at exec | May move to a different domain | Refused unless the new label is a subset of the label held when the bit was set | apparmor_bprm_creds_for_exec(), security/apparmor/domain.c |
setuid(2)/setgid(2) by a privileged task | Allowed | Still allowed — not an execve | — |
Receiving a file descriptor over SCM_RIGHTS | Allowed | Still allowed — not an execve | — |
ptrace(2) of a more-privileged process | Governed by ptrace_may_access() | Unchanged by this flag | kernel/ptrace.c |
The last three rows are the reason the flag’s author was emphatic that it is not a sandbox. Andy Lutomirski, who proposed it, put it bluntly on the list: “no_new_privs is not a sandbox. It’s just a way to make it safe for sandboxes and other such weird things processes can do to themselves safe across execve. If you want a sandbox, use seccomp mode 2, which will require you to set no_new_privs” (Edge 2012).
Why the Gate Sits at execve — the One-Way Ratchet
The danger no_new_privs addresses is specific to execve. Ordinarily, execve is the one moment in a process’s life when its privileges can increase: executing a setuid-root binary raises the effective UID to 0, executing a file with file capabilities raises the permitted capability set, and executing under certain LSM profiles can transition into a more-permissive domain. Every other operation can only hold privilege constant or drop it. execve is the privilege-escalation gateway, and it exists for legitimate reasons — passwd, sudo, and ping are setuid or file-capability binaries that genuinely need to gain privilege.
no_new_privs closes that gateway for a task. Think of it as a ratchet that can turn in only one direction: once set, the task’s privilege ceiling can only stay flat or fall across future execves; it can never rise. The kernel documentation frames it as “a new, generic mechanism to make it safe for a process to modify its execution environment in a manner that persists across execve” (kernel no_new_privs doc). The phrase “persists across execve” is the heart of it: a process wants to install a restriction (a seccomp filter, a Landlock ruleset) that survives exec, but a restriction that survives exec while privilege escalation also survives exec is a security hole. no_new_privs removes the escalation half so the restriction half becomes safe.
flowchart TB START["Task calls<br/>prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)"] START --> SET["set_bit(PFA_NO_NEW_PRIVS,<br/>&task->atomic_flags)<br/>(one-way; no clear path)"] SET --> FORK["fork / clone"] SET --> EXEC["execve of setuid /<br/>file-cap / LSM-transition binary"] FORK -->|"flag inherited"| CHILD["child also has<br/>no_new_privs"] EXEC --> CAP{"commoncap:<br/>bprm->unsafe has<br/>LSM_UNSAFE_NO_NEW_PRIVS?"} CAP -->|"yes"| DOWN["euid := uid, egid := gid<br/>cap_permitted ∩= old permitted<br/>(no escalation)"] DOWN --> SAFE["Privilege ceiling held flat<br/>across exec"] SET --> SECCOMP["Unprivileged seccomp<br/>filter install now permitted"] SET --> LANDLOCK["landlock_restrict_self()<br/>now permitted"] style DOWN fill:#dfd style START fill:#ddf
The lifecycle of the no_new_privs flag. What it shows: setting the flag flips one bit in task_struct->atomic_flags; that bit is inherited by children and consulted by the credential-computation path at every subsequent execve, where it forces effective IDs down to real IDs and caps the permitted set — neutralizing setuid/file-capability escalation. The insight to take: the same flag that makes execve safe is what unlocks unprivileged seccomp and Landlock; they are gated on it because it is the only thing preventing a sandbox escape via a setuid binary.
Two further properties turn a per-task boolean into something a security design can rest on, and they are what make the ratchet metaphor exact rather than decorative: the bit is one-way and it is inherited. Neither is enforced by a runtime check somewhere; both fall out of how the flag is declared and where it is stored, which the next section walks through.
stateDiagram-v2 direction LR [*] --> Clear: task created<br/>PFA_NO_NEW_PRIVS == 0 Clear --> Clear: execve() of a setuid binary<br/>→ euid raised, privilege GAINED Clear --> Set: prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)<br/>set_bit(PFA_NO_NEW_PRIVS) Set --> Set: prctl(..., 1, ...) again → no-op, still set Set --> Set: fork() / clone() → child starts in Set Set --> Set: execve() → flag survives the credential swap Set --> Set: setuid() → flag survives; it is not in struct cred Set --> Set: execve() of a setuid binary<br/>→ euid NOT raised note left of Clear prctl(..., 0, ...) is rejected with -EINVAL: the API has no way to express "clear it". end note note right of Set No TASK_PFA_CLEAR helper is instantiated for this flag, so no kernel code path exists that can clear the bit. end note
The flag as a two-state machine with one absorbing state. What it shows: every transition out of Set loops back into Set — fork, execve, setuid, and a repeated prctl all preserve it — and the only edge into Set is the prctl, with no edge out. The insight to take: irreversibility here is not policed by a permission check that could be bypassed; it is structural. The userspace API refuses the value 0, and the kernel does not instantiate the macro that would generate a clearing helper, so there is literally no code that can turn the bit off. That is why a parent can set it and reason about every descendant it will ever have.
Mechanical Walk-through — From prctl to the Flag and Back
Setting the flag
The userspace entry point is prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0). In the v6.12 kernel/sys.c prctl switch, the handler is strikingly small (kernel/sys.c v6.12):
case PR_SET_NO_NEW_PRIVS:
if (arg2 != 1 || arg3 || arg4 || arg5)
return -EINVAL;
task_set_no_new_privs(current);
break;Walking it line by line: arg2 is the value to set and must be exactly 1 — there is deliberately no way to pass 0, which is how the kernel enforces the one-way property at the API level (you cannot ask to clear it). arg3, arg4, and arg5 must all be zero, a standard prctl hygiene check rejecting calls that set reserved arguments. If any check fails the call returns -EINVAL. Otherwise task_set_no_new_privs(current) sets the bit on the calling task (current), and the call returns success. There is no capability check — any process, however unprivileged, may set its own no_new_privs, which is exactly the point: it can only ever reduce its own future privilege.
The read side, PR_GET_NO_NEW_PRIVS, mirrors it (kernel/sys.c v6.12):
case PR_GET_NO_NEW_PRIVS:
if (arg2 || arg3 || arg4 || arg5)
return -EINVAL;
return task_no_new_privs(current) ? 1 : 0;It takes no arguments (all must be zero) and returns 1 if the flag is set, 0 otherwise.
Where the flag lives
Contrary to a common assumption, no_new_privs is not a field in struct cred and not a discrete named bitfield in struct task_struct. In v6.12 it is bit 0 of the task’s atomic_flags word. The constant and the accessor macros are in include/linux/sched.h (sched.h v6.12):
#define PFA_NO_NEW_PRIVS 0 /* May not gain new privileges. */
TASK_PFA_TEST(NO_NEW_PRIVS, no_new_privs)
TASK_PFA_SET(NO_NEW_PRIVS, no_new_privs)The TASK_PFA_TEST/TASK_PFA_SET macros expand to the inline helpers task_no_new_privs(p) (a test_bit(PFA_NO_NEW_PRIVS, &p->atomic_flags)) and task_set_no_new_privs(p) (a set_bit(...)). Two facts follow directly from this implementation.
First, a TASK_PFA_CLEAR macro does exist in the same header — it is defined immediately below TASK_PFA_SET and generates a clear_bit() helper — and it is instantiated for nearly every other per-task flag. The contrast in the source is stark, and this is the whole “cannot be unset” guarantee visible in six lines:
TASK_PFA_TEST(NO_NEW_PRIVS, no_new_privs)
TASK_PFA_SET(NO_NEW_PRIVS, no_new_privs)
/* <-- no TASK_PFA_CLEAR(NO_NEW_PRIVS, ...) here */
TASK_PFA_TEST(SPREAD_PAGE, spread_page)
TASK_PFA_SET(SPREAD_PAGE, spread_page)
TASK_PFA_CLEAR(SPREAD_PAGE, spread_page)PFA_SPREAD_PAGE, PFA_SPEC_SSB_DISABLE, PFA_SPEC_IB_DISABLE and the rest all get their clearing helper; NO_NEW_PRIVS is the one flag in the list that deliberately does not. There is therefore no task_clear_no_new_privs() anywhere in the tree to call, and the irreversibility is a property of what was not written rather than of a check that could be bypassed. (PFA_SPEC_SSB_FORCE_DISABLE is the other member of the “set-only” club, for the same reason: it exists to make a speculation mitigation permanent.)
Second, because the flag is per-task (in task_struct) rather than per-credential (in cred), it is a property of the thread, not of the identity the thread is currently running under — which is why it survives a setuid() that swaps the credentials and survives execve that installs entirely new credentials. This is a genuinely load-bearing design choice and the opposite of where most privilege state lives: capabilities, UIDs, GIDs, securebits and the LSM label are all in struct cred and are recomputed at every execve, which is exactly the recomputation no_new_privs has to constrain. A constraint stored in the thing being recomputed would be circular. See Process Credentials and struct cred for the wider partition of “what lives in cred and what does not”.
That the flag is in atomic_flags matters for concurrency: it is read and set with atomic bit operations (test_bit/set_bit), so a thread setting its own flag races safely against other threads in the same process reading it.
Enforcement at execve — two separate interventions, not one
Setting the bit is inert until the next execve. What most write-ups then say is “and commoncap.c downgrades the credentials”, which is half the story. In v6.12 the flag is consulted at three distinct points on the exec path, and the setuid case and the file-capability case are handled by different mechanisms in different files — one suppresses the escalation before it is ever proposed, the other lets it be computed and then subtracts it away.
flowchart TB A["execve(prog, argv, envp)<br/>do_execveat_common(), fs/exec.c"] --> B["check_unsafe_exec(bprm)<br/>fs/exec.c:1574"] B --> B1{"task_no_new_privs(current)?"} B1 -->|yes| B2["bprm->unsafe |= LSM_UNSAFE_NO_NEW_PRIVS"] B1 -->|no| B3["(flag not set)"] B2 --> C["bprm_creds_from_file(bprm)"] B3 --> C C --> D["<b>Point 1</b> — bprm_fill_uid(bprm, file)<br/>fs/exec.c:1611"] D --> D1{"task_no_new_privs(current)?"} D1 -->|yes| D2["<b>return immediately</b><br/>S_ISUID / S_ISGID never examined;<br/>bprm->cred->euid is left alone"] D1 -->|no| D3["euid := file owner if S_ISUID<br/>egid := file group if S_ISGID"] D2 --> E["security_bprm_creds_from_file()<br/>LSM hook — calls every module"] D3 --> E E --> F["<b>Point 2</b> — cap_bprm_creds_from_file()<br/>security/commoncap.c:886"] F --> F1["get_file_caps(): reads the<br/>security.capability xattr — this<br/>runs even under no_new_privs"] F1 --> F2{"is_setid OR<br/>__cap_gained(permitted)?"} F2 -->|yes| F3{"bprm->unsafe &<br/>LSM_UNSAFE_NO_NEW_PRIVS?"} F3 -->|yes| F4["euid := uid, egid := gid<br/>cap_permitted ∩= old cap_permitted"] F3 -->|no| F5["escalation stands"] F2 -->|no| G F4 --> G["<b>Point 3</b> — the active LSM's own hook<br/>e.g. apparmor_bprm_creds_for_exec()"] F5 --> G G --> G1{"LSM transition allowed<br/>under no_new_privs?"} G1 -->|"AppArmor: new label not a<br/>subset of the label held at nnp time"| G2["-EPERM, exec fails"] G1 -->|yes| H["commit_creds() — new image runs"]
The three consultation points on the execve path in v6.12. What it shows: check_unsafe_exec() records the flag once into bprm->unsafe; then bprm_fill_uid() in fs/exec.c early-returns so the setuid bits are never even read, while cap_bprm_creds_from_file() in commoncap.c reads the file capabilities anyway and neutralises them afterwards by intersection; finally the active LSM gets its own say and may fail the execve outright. The insight to take: “setuid is ignored” and “file capabilities are ignored” are true statements about the outcome but describe two different implementations — suppression at the source versus subtraction after the fact — and only the third point can make execve fail rather than merely proceed with fewer privileges.
Point 0 — recording the flag. check_unsafe_exec() runs early in do_execveat_common() and translates several “this exec is not fully trusted” conditions into bits in bprm->unsafe. The no_new_privs line is trivially short, and its comment is revealing about intent (fs/exec.c v6.12):
/*
* This isn't strictly necessary, but it makes it harder for LSMs to
* mess up.
*/
if (task_no_new_privs(current))
bprm->unsafe |= LSM_UNSAFE_NO_NEW_PRIVS;“This isn’t strictly necessary” because every hook could call task_no_new_privs(current) for itself; surfacing it as a bprm->unsafe bit puts it next to LSM_UNSAFE_PTRACE and LSM_UNSAFE_SHARE so an LSM author who handles one is likely to handle all three.
Point 1 — setuid, suppressed at the source. bprm_fill_uid() is the function in fs/exec.c that would apply the set-user-ID and set-group-ID mode bits to the proposed credentials. It refuses to do anything at all under the flag (fs/exec.c v6.12):
static void bprm_fill_uid(struct linux_binprm *bprm, struct file *file)
{
...
if (!mnt_may_suid(file->f_path.mnt))
return;
if (task_no_new_privs(current))
return; /* <-- the whole setuid mechanism, skipped */
mode = READ_ONCE(inode->i_mode);
if (!(mode & (S_ISUID|S_ISGID)))
return;
...
if (mode & S_ISUID) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->euid = vfsuid_into_kuid(vfsuid);
}
...
}Note the company that check keeps: the line immediately above it is mnt_may_suid(), the test for the MS_NOSUID mount flag. That is the cleanest one-line summary of the flag’s setuid half — no_new_privs makes every filesystem behave, for this task, as if it were mounted nosuid. It is the same effect, applied per-task instead of per-mount, which is exactly the analogy the LWN coverage used when the flag was proposed: it “would restrict executing binaries in much the same way that the nosuid mount flag works” (Edge 2012). systemd relies on this equivalence in reverse — its documentation notes that when a unit gets a private mount namespace and SELinux is disabled, it mounts everything MS_NOSUID anyway (systemd.exec).
Point 2 — file capabilities, subtracted after the fact. The enforcement for file capabilities is different in kind. It happens in security/commoncap.c, in cap_bprm_creds_from_file(), and the no_new_privs state reaches this function as the LSM_UNSAFE_NO_NEW_PRIVS bit recorded at point 0. Crucially, get_file_caps() runs first and has no no_new_privs check — the security.capability extended attribute is read off disk and turned into a proposed permitted set regardless. The decisive block is (commoncap.c v6.12):
if ((is_setid || __cap_gained(permitted, new, old)) &&
((bprm->unsafe & ~LSM_UNSAFE_PTRACE) ||
!ptracer_capable(current, new->user_ns))) {
/* downgrade; they get no more than they had, and maybe less */
if (!ns_capable(new->user_ns, CAP_SETUID) ||
(bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
new->euid = new->uid;
new->egid = new->gid;
}
new->cap_permitted = cap_intersect(new->cap_permitted,
old->cap_permitted);
}Reading it: is_setid is true when the executable has setuid/setgid bits; __cap_gained(...) is true when file capabilities would raise the permitted set. So this block runs whenever the program being executed would escalate privilege. When LSM_UNSAFE_NO_NEW_PRIVS is set in bprm->unsafe, the inner condition forces new->euid = new->uid and new->egid = new->gid — i.e. the effective IDs are pinned to the real IDs, neutralizing the setuid/setgid bits. And the final line, which runs regardless, intersects the new permitted capability set with the old one (cap_intersect(new->cap_permitted, old->cap_permitted)): the process can come out of execve with no more permitted capabilities than it went in with, never more. The comment in the source says it exactly: “downgrade; they get no more than they had, and maybe less.”
Three details in that block reward a second look. The new->euid = new->uid assignment is redundant under no_new_privs — point 1 already ensured new->euid was never raised — but it is not dead code, because the same block also handles the !ns_capable(new->user_ns, CAP_SETUID) case and the LSM_UNSAFE_PTRACE/LSM_UNSAFE_SHARE cases, which have nothing to do with this flag. no_new_privs is one of several reasons the kernel refuses to honour an escalating exec, and they share one code path. The cap_intersect() line, by contrast, is the only thing stopping file capabilities, and it runs unconditionally inside the outer if — which is why a binary carrying cap_net_bind_service executed under no_new_privs starts with an empty permitted set rather than failing to execute. Finally, note what the function still does afterwards: if (has_fcap || is_setid) cap_clear(new->cap_ambient); clears the ambient set, and further down bprm->secureexec = 1 is set for a privilege-elevated exec, which makes fs/exec.c reset the stack rlimit and sanitise the environment. Under no_new_privs neither escalation happened, so secureexec normally stays 0 — the exec is ordinary because nothing was gained.
Two prose claims are therefore both true but for different reasons, and conflating them is the most common inaccuracy about this flag: setuid is ignored because it is never applied; file capabilities are ignored because they are applied and then intersected away. The observable difference is small but real — under strace, a file-capability binary executed with the flag set still causes the security.capability xattr to be read.
The man page summarizes the same guarantee from the userspace side: “With no_new_privs set to 1, execve(2) promises not to grant privileges to do anything that could not have been done without the execve(2) call (for example, rendering the set-user-ID and set-group-ID mode bits, and file capabilities non-functional).” (PR_SET_NO_NEW_PRIVS(2const)).
Inheritance and irreversibility
Two properties make the flag usable as a security foundation. First, inheritance: “The setting of this attribute is inherited by children created by fork(2) and clone(2), and preserved across execve(2).” (PR_SET_NO_NEW_PRIVS(2const)). Because it lives in task_struct and is copied during task creation and retained across the credential swap of execve, a parent that sets it has effectively locked its entire process subtree. Second, irreversibility: “Once set, the no_new_privs attribute cannot be unset.” (PR_SET_NO_NEW_PRIVS(2const)). There is no prctl to clear it and no kernel code path that clears the bit. Together these mean a sandbox built on no_new_privs cannot be undone by the sandboxed code, even by re-execing itself.
Point 3 — the LSM half, and why it is the vaguest part of the contract
The third clause of the guarantee — “LSMs will not relax constraints after execve” — is the only one the core kernel does not implement itself. no_new_privs hands the LSM a bit and a policy statement, and each module decides what to do with it. That makes this the part of the contract most likely to surprise, and the documentation says so in as many words: “Be careful, though: LSMs might also not tighten constraints on exec in no_new_privs mode. (This means that setting up a general-purpose service launcher to set no_new_privs before execing daemons may interfere with LSM-based sandboxing.)”
AppArmor is the clearest worked example, because its implementation is short enough to read. It does two things. First, it snapshots the label in force at the moment it first sees the flag, in apparmor_bprm_creds_for_exec() (security/apparmor/domain.c v6.12):
/*
* Detect no new privs being set, and store the label it
* occurred under. Ideally this would happen when nnp
* is set but there isn't a good way to do that yet.
*/
if ((bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS) && !unconfined(label) &&
!ctx->nnp)
ctx->nnp = aa_get_label(label);Then, after computing what profile the exec would transition to, it refuses any transition that is not a subset of that snapshot:
/* Policy has specified a domain transitions. If no_new_privs and
* confined ensure the transition is to confinement that is subset
* of the confinement when the task entered no new privs.
*
* NOTE: Domain transitions from unconfined and to stacked
* subsets are allowed even when no_new_privs is set because this
* aways results in a further reduction of permissions.
*/
if ((bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS) &&
!unconfined(label) &&
!aa_label_is_unconfined_subset(new, ctx->nnp)) {
error = -EPERM;
info = "no new privs";
goto audit;
}Three things to take from this. It is a subset test against a snapshot, not against the current label — so a task that entered no_new_privs while unconfined can later transition freely, which is why the !unconfined(label) guard exists and why the comment notes that transitions from unconfined “are allowed even when no_new_privs is set because this always results in a further reduction of permissions.” It fails the execve with -EPERM rather than silently proceeding — this is the one place the flag can stop an exec outright, and it is the mechanism behind “my service will not start after I added NoNewPrivileges=yes”. And the same logic guards AppArmor’s change_hat/change_profile interfaces elsewhere in the file, with the debug string "no_new_privs - change_hat denied", so a confined process cannot escape the snapshot by asking for a profile change either.
SELinux’s behaviour differs and was argued about at the time. Alan Cox worried that blocking LSM transitions could leave a program running in a more-privileged context than the policy intended, but Eric Paris clarified “that SELinux, at least, will still make the same policy decision even without the transition (as it does for nosuid mounts), so that the execution will still fail if the process has the wrong context” (Edge 2012).
Uncertain
Verify: SELinux’s exact v6.12 behaviour when
LSM_UNSAFE_NO_NEW_PRIVSis set — specifically whetherselinux_bprm_creds_for_exec()refuses a domain transition, silently keeps the current domain, or lets the ordinary policy check produce the denial. Reason: I read AppArmor’s implementation directly at v6.12 but did not readsecurity/selinux/hooks.cduring this task; the claim above rests on a 2012 mailing-list summary, which predates the current code by more than a decade. To resolve: readselinux_bprm_creds_for_exec()insecurity/selinux/hooks.cat the target tag and check how it consultsbprm->unsafe. uncertain
Why It Is the Keystone of Unprivileged Sandboxing
The reason no_new_privs exists at all is the confused-deputy / setuid-escape problem in unprivileged sandboxing. Will Drewry’s seccomp-filter work needed a way to let ordinary, unprivileged users install syscall filters. But a syscall filter installed by an unprivileged process and inherited across execve would then apply to a setuid-root binary the process subsequently executes — and as Jake Edge’s LWN write-up of the design puts it, “privilege-changing binaries can get confused when faced with an environment with fewer privileges than are expected. That confusion can lead to privilege escalation or other security holes” (Edge 2012). An unprivileged process must not be able to constrain a more-privileged child, because the child may behave dangerously when its expected syscalls or files are denied.
It is worth making the attack concrete, because “confused deputy” is an abstraction and the actual exploit is a two-line idea. Suppose an unprivileged user could install a seccomp filter that returns EPERM for open() on /etc/shadow, or — more usefully to an attacker — a filter that makes setuid() silently succeed without doing anything. The filter is inherited across execve. The user then execs /usr/bin/passwd, which is setuid-root. passwd starts as root, does its privileged work, then calls setgid()/setuid() to drop back to the invoking user before touching user-controlled data — a near-universal pattern in setuid programs. Under the attacker’s filter, the drop appears to succeed and does not happen. Everything after that point runs as root with the attacker in control.
sequenceDiagram autonumber participant U as Unprivileged attacker participant K as Kernel participant P as /usr/bin/passwd<br/>(setuid root) rect rgb(255, 232, 232) Note over U,P: Without no_new_privs — the attack the interlock exists to stop U->>K: seccomp(SET_MODE_FILTER, filter)<br/>filter forces setuid() → success, no effect K-->>U: filter installed, inherited across execve U->>K: execve("/usr/bin/passwd") K->>P: setuid bit honoured → euid = 0 P->>K: setuid(attacker_uid) "drop privileges" K-->>P: filter says 0 (success) — euid is STILL 0 P->>P: proceeds to handle attacker input as root Note over P: privilege escalation end rect rgb(232, 245, 232) Note over U,P: With no_new_privs — the same sequence, defused U->>K: prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) U->>K: seccomp(SET_MODE_FILTER, filter) K-->>U: permitted, because the flag is set U->>K: execve("/usr/bin/passwd") K->>P: setuid bit IGNORED → euid stays = attacker P->>P: runs as the attacker — the filter now constrains<br/>a process no more privileged than its author Note over P: nothing gained end
Why unprivileged seccomp is gated on no_new_privs, as a before/after trace. What it shows: the danger is not the filter itself but the combination of an inherited restriction with an exec that raises privilege — a setuid program written to trust that its syscalls behave normally becomes the attacker’s deputy. The insight to take: the kernel does not try to make setuid programs robust against hostile syscall behaviour, which would be an unbounded auditing problem across every setuid binary on the system. It removes the other half of the conjunction instead: with no_new_privs set there is no more-privileged child to confuse, so an arbitrary restriction becomes safe to allow. This is the single clearest way to understand what the bit is for.
The kernel’s resolution, then: make the unprivileged sandbox conditional on there being no more-privileged child. If no_new_privs is set, executing a setuid binary no longer elevates — the child is no more privileged than the parent — so it is safe to let an unprivileged parent constrain it. The two facilities are therefore wired together: unprivileged use requires the flag. Linus Torvalds sketched exactly this bargain on the list before either patch existed: “We could easily introduce a per-process flag that just says ‘cannot escalate privileges’. Which basically just disables execve() of suid/sgid programs (and possibly other things too), and locks the process to the current privileges. And then make the rule be that if that flag is set, you can then filter across an execve, or chroot as a normal user, or whatever” (Edge 2012).
Seccomp
The dependency is explicit in kernel/seccomp.c. Installing a filter checks (seccomp.c v6.12):
if (!task_no_new_privs(current) &&
!ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
return ERR_PTR(-EACCES);In words: to install a seccomp filter you must either have no_new_privs set or hold CAP_SYS_ADMIN in your user namespace. An unprivileged process has neither route except the first, so it must set no_new_privs first. The accompanying comment states the threat model directly: installing a filter “requires that the task has CAP_SYS_ADMIN in its namespace or be running with no_new_privs. This avoids scenarios where unprivileged tasks can affect the behavior of privileged children.” The failure errno is -EACCES. (See Seccomp and seccomp-BPF for the filter machinery itself and seccomp and Syscall Filtering for the filter language and dispatch path; the privileged-CAP_SYS_ADMIN bypass exists because an administrator who already has root-equivalent power is trusted not to need the escape protection — though it is poor practice to rely on it.)
There is a second, less-known place seccomp touches the flag: it propagates it. When a filter is installed with SECCOMP_FILTER_FLAG_TSYNC, the filter is pushed onto every other thread of the process, and the flag goes with it (kernel/seccomp.c v6.12):
/*
* Don't let an unprivileged task work around
* the no_new_privs restriction by creating
* a thread that sets it up, enters seccomp,
* then dies.
*/
if (task_no_new_privs(caller))
task_set_no_new_privs(thread);The comment describes a real hole and its closure. Without this line, an unprivileged process could spawn a helper thread, set no_new_privs and install a filter there, TSYNC it onto the siblings, and let the helper exit — leaving the other threads carrying an inherited filter but not the flag, and therefore still able to execve a setuid binary into the filter. This is the one code path in the tree that sets no_new_privs on a task other than current, and it is safe precisely because “threads are considered to be trust-realm equivalent (see ptrace_may_access)”, as the adjacent comment puts it. The lesson generalises: any mechanism that lets a restriction cross from one task to another must carry the flag across too, or it reopens the hole.
Landlock
Landlock makes the same demand. Per the kernel Landlock documentation, landlock_restrict_self() requires that “the task has CAP_SYS_ADMIN in its namespace or is running with no_new_privs” (Landlock doc), for the same reason: a Landlock sandbox that could be escaped by execing a setuid binary would be no sandbox at all. The documentation’s canonical usage sequence sets the flag immediately before locking in the ruleset (Landlock doc):
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("Failed to restrict privileges");
close(ruleset_fd);
return 1;
}
if (landlock_restrict_self(ruleset_fd, restrict_flags)) {
perror("Failed to enforce ruleset");
close(ruleset_fd);
return 1;
}The ordering is mandatory: PR_SET_NO_NEW_PRIVS must succeed before landlock_restrict_self(), or the restriction call would be rejected for an unprivileged caller. The kernel-side gate is a near-verbatim copy of seccomp’s, with a comment saying so — “Similar checks as for seccomp(2), except that an -EPERM may be returned” — and the errno difference is the one thing to remember when debugging: seccomp returns -EACCES, Landlock returns -EPERM for the identical condition (v6.12 security/landlock/syscalls.c).
Landlock also mirrors seccomp’s propagation rule, though later: LANDLOCK_RESTRICT_SELF_TSYNC — Landlock ABI 8, first present in mainline 7.0, not in 6.12 — applies a domain to every thread of the process, and the man page states the same carry-across guarantee: “If the calling thread is running with no_new_privs, this operation enables no_new_privs on the sibling threads as well” (landlock_restrict_self(2)).
Which Subsystems Require or Set the Bit
The bit has accumulated consumers of three kinds — kernel facilities that refuse to work without it, userspace managers that set it on your behalf, and specifications that name it. Keeping them straight matters because the failure signatures differ: a kernel refusal is an errno at install time, a manager setting it is a behaviour change three layers away from anything you wrote.
| Consumer | Relationship to the flag | Failure / effect if absent | Verified against |
|---|---|---|---|
| seccomp filter install (unprivileged) | Requires it, or CAP_SYS_ADMIN in the user namespace | seccomp() / prctl(PR_SET_SECCOMP) returns -EACCES | kernel/seccomp.c v6.12 |
seccomp TSYNC | Propagates it to sibling threads | — (closes a spawn-and-die bypass) | kernel/seccomp.c v6.12 |
landlock_restrict_self() | Requires it, or CAP_SYS_ADMIN | returns -EPERM | security/landlock/syscalls.c v6.12 |
Landlock TSYNC (ABI 8, mainline 7.0) | Propagates it to sibling threads | — | landlock_restrict_self(2) |
| AppArmor exec transitions | Consumes it — refuses non-subset transitions | execve fails with -EPERM, audit info "no new privs" | security/apparmor/domain.c v6.12 |
systemd NoNewPrivileges=yes | Sets it before ExecStart | Service start aborts with exit status 227 / EXIT_NO_NEW_PRIVILEGES if the prctl fails | systemd.exec.xml, exec-invoke.c |
systemd DynamicUser=yes | Implies it, unconditionally and un-disableably | — | systemd.exec.xml |
| systemd seccomp directives | Imply it only when the manager lacks effective CAP_SYS_ADMIN | — (see below) | exec-invoke.c |
OCI runtime spec process.noNewPrivileges | Sets it | Container process can be escalated via an in-image setuid binary | runtime-spec/config.md |
Kubernetes allowPrivilegeEscalation: false | Sets it (inverted sense) | Forced true if the container is privileged or has CAP_SYS_ADMIN | k8s security-context docs |
| Pod Security Standards, Restricted | Mandates allowPrivilegeEscalation: false | Pod rejected / warned by the admission controller | k8s pod-security-standards |
The systemd row deserves unpacking because the rule is more subtle than “seccomp options turn it on”. systemd computes the answer in context_has_no_new_privileges() (src/core/exec-invoke.c):
static bool context_has_no_new_privileges(const ExecContext *c) {
if (c->no_new_privileges)
return true;
if (have_effective_cap(CAP_SYS_ADMIN) > 0) /* if we are privileged, we don't need NNP */
return false;
return context_has_seccomp(c);
}So: an explicit NoNewPrivileges=yes always wins. Otherwise, if the service manager holds effective CAP_SYS_ADMIN — which the system instance normally does — the flag is not set, because systemd can install the seccomp filter through the privileged route instead. Only when it lacks CAP_SYS_ADMIN (the systemd --user instance, or a system service already running with reduced capabilities) does any seccomp-based directive drag the flag in. And context_has_seccomp() is a much wider net than SystemCallFilter= alone:
static bool context_has_seccomp(const ExecContext *c) {
/* We need NNP if we have any form of seccomp and are unprivileged */
return c->lock_personality || c->memory_deny_write_execute ||
c->private_devices || c->protect_clock ||
c->protect_hostname == PROTECT_HOSTNAME_YES ||
c->protect_kernel_tunables || c->protect_kernel_modules ||
c->protect_kernel_logs || context_has_address_families(c) ||
exec_context_restrict_namespaces_set(c) || c->restrict_realtime ||
c->restrict_suid_sgid || !set_isempty(c->syscall_archs) ||
context_has_syscall_filters(c) || context_has_syscall_logs(c);
}LockPersonality=, MemoryDenyWriteExecute=, PrivateDevices=, ProtectClock=, ProtectHostname=, ProtectKernelTunables=, ProtectKernelModules=, ProtectKernelLogs=, RestrictAddressFamilies=, RestrictNamespaces=, RestrictRealtime=, RestrictSUIDSGID=, SystemCallArchitectures=, SystemCallFilter= and SystemCallLog= are all implemented with seccomp under the hood, so any one of them can pull no_new_privs in for an unprivileged manager. This is the concrete mechanism behind “I only added ProtectKernelTunables=yes and now sudo inside my user service is broken.”
Separately and unconditionally, DynamicUser=yes forces it on: “NoNewPrivileges= and RestrictSUIDSGID= are implicitly enabled (and cannot be disabled), to ensure that processes invoked cannot take benefit or create SUID/SGID files or directories” (systemd.exec(5)).
One scoping caveat from the same man page, easy to miss and a frequent source of “but I set it”: “this setting only has an effect on the unit’s processes themselves (or any processes directly or indirectly forked off them). It has no effect on processes potentially invoked on request of them through tools such as at(1), crontab(1), systemd-run(1), or arbitrary IPC services.” A service that hands work to another daemon over D-Bus has not confined that work.
Configuration and Usage Examples
Minimal C — lock the current process before exec:
#include <sys/prctl.h>
#include <stdio.h>
int main(void) {
/* After this, no execve in this process or its children
can raise privilege via setuid bits or file capabilities. */
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
perror("prctl(PR_SET_NO_NEW_PRIVS)");
return 1;
}
/* Demonstrate the one-way nature: this DOES work (re-setting to 1),
but there is no argument value that clears it. */
/* Now exec'ing /usr/bin/passwd would run it WITHOUT setuid-root. */
return 0;
}Line by line: the prctl call sets the flag; a non-zero return indicates failure (only possible if the kernel predates 3.5 or the arguments are malformed). After the call returns, any execve from this process drops setuid/setgid effects — so a subsequent execve("/usr/bin/passwd", ...) would run passwd as the invoking user, not root, and passwd would fail when it tried to write /etc/shadow.
Reading the flag:
int nnp = prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0); /* 1 if set, 0 if not */Observing it on a running process: the per-thread status file exposes NoNewPrivs:
$ grep NoNewPrivs /proc/self/status
NoNewPrivs: 0
A 1 there means the thread is running under no_new_privs; this is how you confirm a container runtime or systemd unit applied it. Because the file is per-thread — /proc/<pid>/task/<tid>/status for each — and because the flag is inherited rather than granted, the useful diagnostic is to walk up the process tree until you find the ancestor where it reads 0. That ancestor’s child is where the bit was set, and that is the configuration you have to change.
flowchart TB I["PID 1 — systemd<br/>NoNewPrivs: 0"] --> S["my-daemon.service<br/>NoNewPrivileges=yes<br/><b>prctl() here</b><br/>NoNewPrivs: 1"] I --> S2["other.service<br/>NoNewPrivs: 0"] S --> W1["worker (fork)<br/>NoNewPrivs: 1"] S --> W2["helper (fork + execve)<br/>NoNewPrivs: 1"] W2 --> H["execve('/usr/bin/sudo')<br/>setuid bit ignored<br/>NoNewPrivs: 1 — fails"] W1 --> T["pthread_create()<br/>NoNewPrivs: 1<br/>(inherited via clone)"] S2 --> Z["execve('/usr/bin/sudo')<br/>setuid honoured — works"] I -.->|"systemd-run --user, at(1),<br/>cron, a D-Bus activated service:<br/>NOT a descendant"| ESC["new process under PID 1<br/>NoNewPrivs: 0 — escapes the unit"] S -.-> ESC
Inheritance drawn on a real process tree, and the one hole in it. What it shows: every descendant of the task that called the prctl carries the bit — through fork, through clone for threads, and through execve — while a sibling branch of the tree is entirely unaffected, so the flag partitions the tree at exactly one node. The insight to take: the dashed edge is the hole worth knowing about. Work handed off to at(1), cron, systemd-run, or a D-Bus-activated service is not forked from your process, so it is not a descendant and does not inherit anything — systemd’s own manual warns of exactly this. Confinement follows the process tree, and IPC is a way to leave the tree.
systemd unit: the service manager exposes it as NoNewPrivileges=yes, which “ensures that the service process and all its children can never gain new privileges through execve() (e.g. via setuid or setgid bits, or filesystem capabilities)” and defaults to false (systemd.exec(5)). The prctl is issued from exec_invoke() just before the seccomp filters are applied, and a failure aborts the service with exit status 227 (EXIT_NO_NEW_PRIVILEGES). It is also implied in two situations — unconditionally by DynamicUser=yes, and conditionally by any seccomp-backed directive when the manager is unprivileged — which is worked out against the systemd source in the table above.
[Service]
ExecStart=/usr/libexec/my-daemon
# Explicit, and the one that always wins:
NoNewPrivileges=yes
# Each of these is seccomp-backed and will imply the above
# on an unprivileged manager (e.g. systemd --user):
SystemCallFilter=@system-service
RestrictSUIDSGID=yes
LockPersonality=yes
# And this one implies it unconditionally, and cannot be turned off:
DynamicUser=yesFailure Modes and Common Misunderstandings
“My sudo/ping/passwd stopped working inside the sandbox.” This is no_new_privs working as designed, not a bug, and it is by a wide margin the most common practical encounter with the flag. Any setuid-root or file-capability helper executed under it runs without its elevated privilege and fails at the first privileged operation. The symptom differs by tool, which is why it is often misdiagnosed:
| Tool | Mechanism it relies on | Symptom under no_new_privs |
|---|---|---|
sudo | setuid-root | sudo: effective uid is not 0, is /usr/bin/sudo on a file system with the 'nosuid' option set... — the error literally blames nosuid, because the effect is identical |
ping (modern) | cap_net_raw file capability | socket: Operation not permitted when opening the raw/ICMP socket |
passwd | setuid-root | fails writing /etc/shadow with EACCES |
newuidmap / newgidmap | cap_setuid/cap_setgid file capabilities | rootless container setup fails to write the ID maps |
mount, su, pkexec | setuid-root | EPERM from the privileged syscall |
dumpcap, chrome-sandbox | file capabilities / setuid | capture or sandbox-helper startup fails |
Note the sudo message especially: it names nosuid because the kernel implements the setuid half of no_new_privs in the same place it implements MS_NOSUID, so the two are genuinely indistinguishable from userspace. Anyone debugging this from the error text alone will go looking at /proc/mounts and find nothing wrong.
flowchart TD S["A setuid or file-capability helper<br/>fails with EPERM / EACCES"] --> C1{"grep NoNewPrivs<br/>/proc/PID/status"} C1 -->|"NoNewPrivs: 0"| O["Not this flag.<br/>Check MS_NOSUID on the mount,<br/>the capability bounding set,<br/>SELinux/AppArmor denials,<br/>or a seccomp SIGSYS."] C1 -->|"NoNewPrivs: 1"| C2{"Where did the bit<br/>come from?"} C2 --> P1["Your own prctl()"] C2 --> P2["An ancestor's prctl()<br/>— check the parents' status files<br/>up the tree until one reads 0"] C2 --> P3["systemd: NoNewPrivileges=,<br/>DynamicUser=, or a seccomp<br/>directive on an unprivileged manager"] C2 --> P4["Container runtime:<br/>OCI noNewPrivileges / Docker<br/>--security-opt / k8s<br/>allowPrivilegeEscalation: false"] P1 --> F["Fix: it cannot be cleared.<br/>Restructure so the privileged work<br/>happens BEFORE the prctl,<br/>or delegate it to a helper process<br/>started outside the sandbox."] P2 --> F P3 --> F2["Fix: set NoNewPrivileges=no explicitly<br/>(it overrides the implication),<br/>or drop the seccomp directive,<br/>or move the helper out of the unit."] P4 --> F3["Fix: allowPrivilegeEscalation: true<br/>— but the Restricted Pod Security<br/>profile forbids it, so prefer<br/>removing the setuid dependency."]
A diagnostic path for the single most common no_new_privs incident. What it shows: the first question is always the /proc/<pid>/status line, because it distinguishes this flag from the four other things that produce the same errno; the second is provenance, because the bit is usually inherited from somewhere you did not write. The insight to take: there is no “turn it off here” fix once the bit is set on a live task — every branch ends either in restructuring the program so the privileged step precedes the prctl, or in changing the configuration that set it one level up. Design for this by setting the flag as late as possible.
“I’ll just unset it when I’m done.” You cannot. There is no clearing prctl and no kernel path that clears the bit (PR_SET_NO_NEW_PRIVS(2const)). The flag persists for the life of every task that inherited it. Design accordingly: set it as late as possible, after all legitimate privilege transitions are complete.
Confusing no_new_privs with dropping capabilities. They are orthogonal and complementary. Dropping a capability from the bounding set removes it now; no_new_privs prevents regaining any privilege via execve later. A process can drop all capabilities yet still escalate by execing a setuid-root binary — unless no_new_privs is also set. The two together are what actually pins a privilege ceiling. See Capability Sets and the Bounding Set and CAP_SYS_ADMIN and the Capability Granularity Problem.
Assuming it constrains the current execve. It does not affect operations already in progress, only future execve calls. Set it before, not after, the exec you want to neutralize.
The LSM caveat cuts both ways. The kernel doc warns that under no_new_privs, “LSMs will not relax constraints after execve” and conversely cautions that “LSMs might also not tighten constraints on exec in no_new_privs mode. (This means that setting up a general-purpose service launcher to set no_new_privs before execing daemons may interfere with LSM-based sandboxing.)” (no_new_privs.rst v6.12). The flag stops execve from relaxing an LSM domain (good for sandboxing), but AppArmor implements this as a subset test against a snapshot, so a transition the administrator intended as a tightening can be refused outright with -EPERM if the new label is not comparable to the snapshot — the exec fails, the service does not start, and the audit record says "no new privs". See the LSM subsection above for the code. This is the sharpest edge in the whole design: a hardening flag and a hardening profile can cancel each other into a service that will not run.
prctl returns EINVAL and you assume the kernel is too old. The handler rejects any arg2 other than 1 and requires arg3/arg4/arg5 to be zero. Passing prctl(PR_SET_NO_NEW_PRIVS, 1) from C is fine because variadic promotion zeroes nothing — the remaining arguments are garbage from the stack, not zeros. Always write the full five-argument form prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); the intermittent EINVAL this produces otherwise is routinely misread as “kernel predates 3.5”.
Setting it in one thread does not cover the siblings. The flag lives in task_struct, and in Linux a thread is a task, so prctl sets it on the calling thread only. A program that sets it in main() after starting a worker pool has covered one thread. Two mechanisms propagate it deliberately — seccomp’s TSYNC and, from Landlock ABI 8 (mainline 7.0), LANDLOCK_RESTRICT_SELF_TSYNC — and outside those you must call the prctl on every thread yourself.
Expecting it to stop ptrace. It does not, and this was raised and dismissed when the flag was proposed. Alan Cox pointed out that a no_new_privs “sandbox” can still be subverted via ptrace(2); the answer was that this misunderstands the goal — “PR_SET_NO_NEW_PRIVS + chroot + ptrace is exactly as unsafe as ptrace without PR_SET_NO_NEW_PRIVS. Neither one allows privilege escalation beyond what you started with” (Edge 2012). If you need to stop ptrace, that is a seccomp filter (or a Landlock domain, which restricts tracing structurally).
Assuming it makes a container root-safe. It closes the setuid/file-capability escalation path inside the container. It does nothing about a container that was granted CAP_SYS_ADMIN, a privileged container, a writable /proc or host mount, or a kernel vulnerability. Kubernetes makes the first of these explicit by forcing allowPrivilegeEscalation to true when the container is privileged or holds CAP_SYS_ADMIN — the flag and those grants are mutually exclusive by construction.
Alternatives and When to Choose Them
no_new_privs is not a sandbox by itself — it grants no restriction on its own; it only enables other restrictions to be applied safely by unprivileged processes. The decision is therefore not “no_new_privs vs X” but “which restriction do I layer on top of no_new_privs”:
- For syscall-surface restriction → Seccomp and seccomp-BPF, which requires the flag for unprivileged use.
- For filesystem/network object restriction → Landlock, which requires the flag.
- For administrator-authored, system-wide policy → SELinux / AppArmor via the LSM framework, which do not require
no_new_privsbecause they are installed by privileged administrators, not by the sandboxed process itself. This is the key distinction: MAC policies are a privileged mechanism;no_new_privs-gated seccomp/Landlock are the unprivileged path to self-confinement.
If you are root and authoring system policy, you may not need no_new_privs at all. If you are an unprivileged application sandboxing itself, no_new_privs is mandatory and comes first.
Production Notes
Container runtimes apply no_new_privs widely, and the plumbing is worth tracing end to end because each layer renames it. The OCI runtime specification carries process.noNewPrivileges, “(bool, OPTIONAL) setting noNewPrivileges to true prevents the process from gaining additional privileges”, and it appears in the spec’s own example configurations with the value true (runtime-spec config.md). Docker surfaces it as --security-opt=no-new-privileges. Kubernetes exposes the inverse: securityContext.allowPrivilegeEscalation, which “directly controls whether the no_new_privs flag gets set on the container process”, with the important caveat that it “is always true when the container is run as privileged, or has CAP_SYS_ADMIN” (Kubernetes security-context docs) — which mirrors the kernel’s own CAP_SYS_ADMIN bypass in seccomp and Landlock, and means you cannot get the flag by asking for it while also asking for CAP_SYS_ADMIN. The orchestration translation is covered in SecurityContext.
The Pod Security Standards make it non-optional at the top tier: the Restricted profile lists spec.containers[*].securityContext.allowPrivilegeEscalation (and the same for init and ephemeral containers) as a restricted field whose only allowed value is false, under the rubric “Privilege escalation (such as via set-user-ID or set-group-ID file mode) should not be allowed” — a Linux-only control since v1.25 (Pod Security Standards). The Baseline profile does not require it. The practical effect is that a container running under Restricted cannot be escalated to root by an attacker who finds a setuid-root binary inside the image — and images are full of them, because base images ship sudo, su, mount, passwd, and ping by default.
The flag composes with, but does not replace, dropping capabilities. A container that drops all capabilities but leaves allowPrivilegeEscalation: true can still regain them by executing a file-capability binary from the image; a container with the flag set but a full capability set has capabilities it never needed. The pair is what actually pins the ceiling, which is why hardened baselines set both.
Browsers were the original heavy users, and the flag exists largely because of them: Will Drewry’s seccomp-filter work was targeted at “the Chrome/Chromium web browser in order to sandbox untrusted code”, with QEMU, OpenSSH, and vsftpd named as interested parties (Edge 2012). Chrome’s renderer sandbox performs exactly the sequence the flag was designed for — set the flag, install the filter, then run untrusted code.
There is a second, quieter production use the kernel documentation calls out and that is easy to overlook, because it involves no sandbox at all: no_new_privs on its own reduces attack surface for an entire UID. “By itself, no_new_privs can be used to reduce the attack surface available to an unprivileged user. If everything running with a given uid has no_new_privs set, then that uid will be unable to escalate its privileges by directly attacking setuid, setgid, and fcap-using binaries; it will need to compromise something without the no_new_privs bit set first” (no_new_privs.rst). Setting the flag in a login session’s PAM stack or a service-account launcher removes the entire class of local-privilege-escalation bugs in setuid binaries from that account’s reach, without any filter, profile, or policy — at the cost, of course, of everything in the diagnostic table above.
Finally, the kernel doc closes with a forward-looking note that has partly come true and is a good way to remember what the flag is: “In the future, other potentially dangerous kernel features could become available to unprivileged tasks if no_new_privs is set. In principle, several options to unshare(2) and clone(2) would be safe when no_new_privs is set, and no_new_privs + chroot is considerably less dangerous than chroot by itself.” Landlock, merged nine years after the flag, is precisely an instance of that pattern: a facility that would have been unsafe to expose to unprivileged users, made safe by requiring the bit first.
See Also
- POSIX Capabilities — the capability sets whose
execve-time escalationno_new_privsneutralizes - Capability Transitions Across execve — the exact credential recomputation at
execvethat the flag modifies (thecommoncap.cpath) - Capability Sets and the Bounding Set — the complementary “drop now” mechanism;
no_new_privsis the “never regain” mechanism - CAP_SYS_ADMIN and the Capability Granularity Problem — sibling; why dropping capabilities (and pinning them with this flag) matters
- Seccomp and seccomp-BPF — requires
no_new_privsfor unprivileged filter installation (-EACCESotherwise) - Landlock — requires
no_new_privsbeforelandlock_restrict_self() - seccomp and Syscall Filtering — the filter language, the entry-path dispatch, and
TSYNC, which is the one mechanism that sets this flag on a task other thancurrent - AppArmor — the LSM whose exec-transition behaviour under this flag is documented above, and the most likely source of a
-EPERMatexecve - User Namespaces — the other route to “privilege without privilege”, and the one
no_new_privsdoes not gate - Process Credentials and struct cred — note that the flag lives in
task_struct, notcred, which is why it survives credential changes - Linux Containers and Isolation MOC — containers set this flag as a core hardening step (
noNewPrivilegesin the OCI spec); this title is cross-linked verbatim from there - Linux Security MOC — the parent map; this note is canonical under §B and the “keystone of unprivileged sandboxing” cross-cutting theme