pledge(2), or exploring a cool OpenBSD security feature

Posted on Aug 15, 2026

puffy-79 OpenBSD 7.9 official artwork

While browsing Youtube (as on does on PTO), I stumbled upon this video by Nir Lichtman presenting the pledge(2) syscall.

It seemed like an interesting concept I wanted to experiment with on my own and so I span up a fresh OpenBSD 7.9 VM and started writing some code.

'When we make a pledge, we mean it'

This quote from George W. Bush (not that I like the guy) perfectly embodies the purpose of the pledge(2) syscall, which first appeared in OpenBSD 5.9.

OpenBSD’s man pages describe it as follow:

The pledge() system call separates the POSIX feature set into a group of approximately 3 dozen subsystems. By calling pledge() the program can declare which subsystems it will need in the future in a space-separated string called promises. Subsystems not listed become unavailable, and most attempts to use operations in that subsystem result in the process being killed with an uncatchable SIGABRT, delivering a core file if possible.

This means that a developper can, at write-time, define and limit which syscalls a program is allowed to make. This is particularly useful, say, to reduce or limit the impact a vulnerability can have.

Furthermore:

Subsequent calls to pledge() can reduce the subsystems which still work, but previously revoked subsystems cannot be re-activated.

Wait. We can also limit which syscalls a program is allowed to make during it’s execution ? This seems all the more interesting !

Hello, World!

Let’s start with the age-old Hello world program to familiarize ourselves with this function:

#include <unistd.h>
#include <stdio.h>

int main(void) {
    pledge("stdio", NULL);
    
    printf("Hello, World!\n");   

    return 0;
}

The pledge(2) syscall takes two arguments, as per it’s definition:

int pledge(const char *promises, const char *execpromises);

The first one is a pointer a string containing the "promises" we make, meaning which family of syscalls we allow. The second dictates the permissions that will be applied to a new process spawned if the current program calls execve(2). It uses the exact same keywords as the promises field.

We compile and run it, and we can see that everything works as expected:

Now, let’s remove "stdio", leaving only an empty string (NULL negates pledge(2)’s ability to deny any syscalls):

#include <unistd.h>
#include <stdio.h>

int main(void) {
  
  pledge("", NULL);
    
  printf("Hello, World!\n");

  return 0;
}

On the contrary, let’s keep stdio and try to perform other syscalls not covered by the stdio pledge group:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int main(void) {
  pledge("stdio", NULL);

  printf("Hello, World!\n");

  int pid = fork();
  kill(pid, 9);

  return 0;
}

In both cases, the program crashes when not unauthorized syscalls are performed.

In our last example, both fork(2) and kill(1) are part of the proc pledge group. Notice, however, that the program crashed on the first violating syscall it encountered.

Reducing permissions during execution

An awesome aspect of pledge(2) we touched on earlier is that we can reduce the program’s ability to perform syscalls during its execution. Let’s start from our previous example and add another call to pledge(2):

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

int main(void) {
  pledge("stdio proc", NULL);  
  
  printf("Hello, World!\n");

  int pid = fork();
    
  pledge("stdio", NULL);
    
  kill(pid, 9);

  return 0;
}

This time, we can see that the syscall made by the kill(1) function was denied, but not the one made by fork(2). This means that the permissions were successfully dropped during execution.

⚠️ Warning
Once decreased, permissions offered by pledge() cannot be increased again.

Other exploit mitigations

OpenBSD has some of the most advanced exploit mitigations in any OS (though their effectiveness is subject to debate):

  • W^X in the kernel, cannot be disabled. Must mount the file system with a specific option wxallowed. On a default, fresh install, /usr/local is the only partition where this isn’t enforced.

  • ASLR that cannot be disabled (unlike the Linux kernel).

  • Syscalls are only authorized from libc, thanks to msyscall(2) and pinsyscalls(2)

The latter can easily be demonstrated by writting a small program in assembly, as pointed to by this comment on Reddit:

.globl main
.section .text
main:
    mov $4, %rax
    mov $1, %rdi
    mov $24, %rdx
    lea message(%rip), %rsi
    syscall
    ret

.section .rodata
message:
    .string "Syscall pining is cool!\n"

This code, however, works:

.global main

.section .text
main:
    mov $4, %rax
    mov $1, %rdi
    mov $24, %rdx
    lea message(%rip), %rsi

1:  
    syscall
    ret

.section .openbsd.syscalls,"",%progbits
    .long 1b
    .long 4

.section .rodata
message:
    .string "Syscall pining is cool!\n"

So shellcodes and ROP exploits are (almost) out of the question, which is truly fascinating.

Final words

Overall, I found the pledge(2) syscall to be wayyy easier to work with than Linux’s seccomp, which is always a plus for devs. However, it isn’t imune to classic LD_PRELOAD tricks, unless you statically link your binaries.

Playing with OpenBSD made me realize how much I needed such a simple, sane and secure OS in my life. While I’m not fully aligned with the BSD-style licences philosophy (I’m more of a GPL guy), I think OpenBSD has a place in my infrastructure (DNS, load balancer and syslog servers for instance).

See ya !