pledge(2) and unveil(2): what OpenBSD figured out that Linux is still catching up to
Published by RodHat

Linux grew seccomp-bpf in 2012. You write BPF programs that filter syscalls. This requires you to understand BPF, know which syscalls your code makes (including libraries you didn’t write), and maintain that filter as the code evolves. In 2021, Linux added Landlock for filesystem access control. You create rulesets, add rules per path, call landlock_restrict_self(2). Also requires prctl(PR_SET_NO_NEW_PRIVS, ...) first. Between the two you can build a reasonable sandbox. You’ll need namespaces for the rest.
OpenBSD 5.9 (May 2016) shipped pledge(2). One syscall, one string.
if (pledge("stdio rpath inet dns", NULL) == -1)
err(1, "pledge");
That’s the entire API surface for sandboxing a DNS-resolving network client. After this line, the process can do stdio operations, read from the filesystem, open TCP/UDP sockets, and resolve hostnames. Anything else: SIGABRT, process dead, kernel logs a pledge violation. You don’t get a second chance to misconfigure it.
What the promise string does
The promise string is space-separated keywords. Each keyword enables a category of syscalls:
stdioenables the baseline: read, write, seek, dup, pipe, select, poll, clock_gettime, getpid, and about forty others that any process doing I/O needs. This one is almost always in the list.rpathenables filesystem reads: open with O_RDONLY, stat, readdir, readlink. No writes.wpathenables filesystem writes: open with O_WRONLY or O_RDWR, truncate, chmod, chown.cpathenables creation and removal: open with O_CREAT, mkdir, rmdir, unlink, rename. Requires wpath too if you’re writing to the files you create.inetenables IPv4 and IPv6 socket operations: socket(AF_INET), connect, send, recv, bind, listen, accept.dnsis a special one: it enables the system resolver, not raw sockets.getaddrinfo()works; opening raw sockets to port 53 does not. If you want name resolution without opening arbitrary inet sockets, you usednswithoutinet.procenables fork, exec, kill, setpgid, setsid. Forking daemons need this.execenables execve. Often paired withproc. Note: a child exec’d from a pledged process does not inherit the pledge unless the parent passed the second argument topledge()(theexecpromisesstring).sendfdandrecvfdenable fd passing over unix sockets. Privilege-separated daemons use these to hand off fds between a privileged parent and an unprivileged worker.ttyenables terminal operations: tcsetattr, ioctl for terminal ioctls. Interactive programs need this.idenables setuid, setgid, setgroups. Drop privileges after pledge if you need to.
About fifty categories total cover everything from audio (audio) to crypto hardware (crypto) to tape devices (tape). The source is in sys/kern/kern_pledge.c if you want the full list with the exact syscalls each category enables.
One important behavior: you can call pledge() multiple times, but you can only restrict further. You cannot add promises back. A daemon that calls pledge("stdio rpath inet dns", NULL) after setup, then tries to call pledge("stdio rpath inet dns wpath", NULL) later, dies with a violation. The model is: start with what you need, narrow down as startup phases complete.
A real example: a small HTTPS client daemon
#include <stdio.h>
#include <err.h>
#include <unistd.h>
int main(void) {
/* parse config file while we still can */
FILE *cfg = fopen("/etc/myapp/config", "r");
if (!cfg) err(1, "fopen config");
parse_config(cfg);
fclose(cfg);
/* open log file before locking down */
FILE *log = fopen("/var/log/myapp.log", "a");
if (!log) err(1, "fopen log");
/* done with setup: lock down to what the daemon actually needs */
if (pledge("stdio inet dns", NULL) == -1)
err(1, "pledge");
/* from here: no filesystem access, no exec, no fork */
run_daemon(log);
return 0;
}
The config read and log file open happen before pledge. After the pledge call, the daemon cannot open any new files. The already-open log fd is fine because stdio covers writes to existing fds. inet and dns cover the network operations. If the code in run_daemon() tries to call system(3) or fopen() or anything outside those three categories, the process is killed.
The trick is the ordering: do privileged or broad operations first, narrow the pledge before starting the main loop. This is the same pattern as dropping root: do the privileged work, then drop.
unveil(2)
OpenBSD 6.4 (October 2018) added unveil(2). It handles the filesystem axis that pledge handles at the syscall-category level.
unveil("/var/www/htdocs", "r");
unveil("/var/log/httpd", "w");
unveil(NULL, NULL); /* lock the unveiled tree */
After the unveil(NULL, NULL) call, the process’s view of the filesystem is reduced to exactly the paths you listed. Trying to open /etc/passwd returns ENOENT, not EACCES. This is deliberate: the visibility of the path is revoked, not just the permission. An attacker who has compromised the process cannot even determine whether /etc/shadow exists. /proc enumerations, config file hunting, anything outside the unveiled tree: the kernel lies and says it’s not there.
The permission string is single characters: r (read), w (write), x (execute binaries), c (create files). Combinations work: "rw" gives read-write. A web server serving static files only needs r on the docroot.
/* privilege-separated httpd: after fork, worker gets docroot only */
void worker_setup(const char *docroot) {
if (unveil(docroot, "r") == -1)
err(1, "unveil docroot");
if (unveil(NULL, NULL) == -1)
err(1, "unveil lock");
if (pledge("stdio rpath inet sendfd", NULL) == -1)
err(1, "pledge");
}
Like pledge, you can call unveil() multiple times before locking. You can add paths. You can also call it again with a path already in the tree to change the permissions on that path, but only to restrict further. After unveil(NULL, NULL), the tree is frozen.
The combination of pledge and unveil covers both dimensions: what syscall categories are allowed (pledge) and which filesystem paths are visible (unveil). A process that handles user uploads to /var/uploads and logs to /var/log/myapp and does nothing else on disk can be locked to exactly those two directories with exactly the right permissions each.
Violation behavior
When a pledge violation happens, the kernel delivers SIGABRT to the process and logs the violation to syslog. The log line includes the process name, pid, uid, the violated promise category, the syscall that triggered it, and the arguments. You get a precise record of what the code tried to do that it wasn’t supposed to.
This is brutal in the right way. There’s no “try without pledge first, add promises as needed” mode in production. You test with pledge, you watch for violations in the log during development, you fix them, and you ship a process that is correct or dead. The alternative, where a constraint violation is a returned error you can handle in code, leads to code that handles it by disabling the constraint. OpenBSD does not offer that path.
On FreeBSD, Capsicum is the closest equivalent. cap_rights_limit(fd, rights) restricts what operations a specific fd allows, and cap_enter() puts the process into capability mode where it cannot open new paths without a privileged parent handing it an fd. Capsicum is more granular than pledge (per-fd rights vs. per-category for the whole process) and more work to retrofit into existing daemons. OpenBSD’s base system was retrofitted: nearly every daemon in the system calls pledge now. That mass retrofit across a full OS is evidence that pledge’s ergonomics are right.
What you’re comparing
If you’re writing a network daemon on Linux in 2026, building a comparable sandbox requires: seccomp-bpf to restrict syscalls (write the BPF filter, or use libseccomp), Landlock to restrict filesystem paths (create ruleset, add rules, landlock_restrict_self()), and optionally namespaces for the rest. Three mechanisms, three APIs, three surfaces to get wrong.
On OpenBSD, you write one pledge call and one block of unveil calls. Both syscalls are in every OpenBSD 6.4+ base. No libraries required.
Linux’s mechanisms are more composable and more granular. Seccomp-bpf can filter at the argument level, not just the syscall level. Landlock has per-access-type permission bits. Namespaces can isolate network stacks entirely. If you need that precision, Linux gives you it. If you want something you can bolt onto a daemon in an afternoon and not get wrong, OpenBSD’s model is considerably less rope.
The seccomp-bpf post covers Linux’s syscall-filtering side, and the Landlock post covers Linux’s filesystem restriction layer, the two together being roughly what pledge and unveil are on OpenBSD.