$RodHat_
MOTD

pledge() turns 10. Linux still doesn't have anything half as clean.

Published by

pledge() turns 10. Linux still doesn't have anything half as clean.
Photo: AI-generated — no human photographer / RodHat AI Cover

OpenBSD 5.9 shipped in April 2016 with a new syscall: pledge(2). A process calls it to declare what it intends to do for the rest of its life — a space-separated string of capability names like "stdio rpath dns inet" — and the kernel enforces that contract from that point forward. Call something outside your declared set and you get SIGABRT. The pledge can be reduced but never expanded. One-way ratchet. Simple enough to read in a diff review without stopping to think.

Ten years old this spring. I’ve been watching the Linux side try to get there for most of that decade.

What pledge actually does

The API is almost offensively simple:

/* after parsing args, before doing anything interesting */
if (pledge("stdio rpath", NULL) == -1)
    err(1, "pledge");

That’s it. The process has now told the kernel: “I read files, I write to stdio, I do nothing else. Hold me to it.” If the code later tries to open a socket — bug, exploit, confused logic, whatever — SIGABRT, journal entry, done. The kernel didn’t need to understand the program’s intent; the program told it.

unveil(2) followed in OpenBSD 6.4 (2018) and completes the picture at the filesystem layer. Before calling unveil(NULL, NULL) to lock the policy, the process enumerates exactly which paths it needs:

unveil("/etc/ssl/cert.pem", "r");
unveil("/tmp", "rwc");
unveil(NULL, NULL);  /* lock */

After that lock, anything not explicitly unveiled doesn’t exist. Not “EPERM.” Not “EACCES.” The path simply isn’t there. The VFS-level restriction is why this is interesting — directory traversal tricks, symlink games, and relative-path shenanigans that beat a naive file-descriptor check don’t work because the kernel never finds the path in the first place.

Every piece of the OpenBSD base that handles untrusted data uses both: sshd, ssh, httpd, smtpd, the DNS stub resolver, bgpd, cu. The pattern is so idiomatic that reviewing a new utility and seeing it not call pledge is itself a red flag in the project’s code review culture.

What Linux has

seccomp-bpf has been in the kernel since 3.5 (2012). It’s more powerful than pledge — you can allow syscalls conditionally on their arguments, not just by name — and that power is exactly what makes it brutal to use correctly. The “write an allow-list for a real program” exercise with raw seccomp looks like this:

scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL_PROCESS);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fstat), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
/* ... and eight more you forgot until the first time it segfaults in prod ... */
seccomp_load(ctx);

This is not a complaint about libseccomp — it’s a solid library. The complaint is architectural. The model requires you to enumerate every syscall your process will touch including the ones that libc calls on your behalf when doing things you think are simple. Get one wrong and your process dies in a way that looks like a crash, not a sandbox violation. Writing a correct seccomp policy for a non-trivial program is a research task, not a review-time audit. So almost nobody does it, even when they should.

Landlock arrived in kernel 5.13 (2021). This is the genuine unveil analog for Linux, and it’s actually good. The API is more verbose — you set up a ruleset, add access rights to it, and restrict with landlock_restrict_self() — but it’s doing the right thing at the VFS layer, and it doesn’t require root. A process can sandbox itself:

struct landlock_ruleset_attr attr = {
    .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE
                       | LANDLOCK_ACCESS_FS_READ_DIR,
};
int ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
/* add path-specific rules... */
landlock_restrict_self(ruleset_fd, 0);

It’s working. Adoption is growing — systemd uses it for some unit sandboxing now, and browser sandboxes are wiring it in. The kernel team is actively extending the access right set each cycle. By 6.16 you can restrict network access with it in addition to filesystem. The direction is right.

But it’s 2026 and I’d still bet on “most C daemons on Linux handle untrusted input without any self-imposed sandbox” being true by a wide margin. pledge is in every OpenBSD utility because it’s one readable line with obvious semantics. Landlock, even with a wrapper library, is twenty lines that most maintainers will add to the backlog and ship when they get around to it.

The real gap isn’t implementation. It’s culture and API surface.

OpenBSD’s security culture is coercive in a useful way. The project’s internal code review norms treat an unexplained missing pledge call the way Linux reviews treat an unexplained printk(KERN_ERR — as something that needs justification. The tooling enforced adoption; the adoption created expertise; the expertise made the per-process security model actually mean something at the fleet level.

Linux doesn’t have that coercive norm because Linux doesn’t have a coherent application layer. It has a kernel used by ten thousand distributions making independent decisions. The mechanism exists; the social pressure to use it doesn’t.

This is the same dynamic that’s kept unprivileged eBPF as a default for years longer than it should have been and kept sudo’s credential-caching model unchallenged until run0 showed up to argue. The technical fix is usually not the hard part. The hard part is making the secure path also be the path of least resistance for the developer writing the code.

The point

If you’re writing a daemon in C that handles untrusted input and it’s not calling pledge and unveil equivalents — or Landlock, or a seccomp profile, or at minimum running inside a systemd unit with SystemCallFilter and PrivateTmp — you have an attack surface that didn’t need to exist. The io_uring structural CVE pattern isn’t unique to io_uring; it’s what happens when powerful kernel features are accessible without countervailing restriction on what the process receiving them is allowed to do.

OpenBSD built the cleaner solution first. Landlock is catching up. Use whichever one you’ve got, because “I’ll add sandboxing later” has the same expected completion date as “I’ll add tests later.”


For the syscall-level mechanics of why filesystem namespace restrictions work the way they do — what VFS lookup actually does, why path resolution is the right interception point, what makes symlink games work against naive access controls — The Linux Programming Interface covers the VFS layer in detail. Chapter 18 on directories and links and Chapter 15 on file attributes are the foundation for understanding why Landlock’s approach is sound. For the OpenBSD side, the pledge(2) and unveil(2) man pages are genuinely worth reading as design documents — they’re short, precise, and explain the design decisions inline, the way man pages are supposed to work.