1. Introduction §
This article is meant to be a simple guide explaining how to make use of the OpenBSD specific feature pledge in order to restrict a software capabilities for more security.
While pledge falls in the sandboxing features, it's different than the traditional sandboxing we are used to see because it happens within the source code itself, and can be really tightened. Actually, many programs requires lot of privileges like reading files, doing DNS etc... when initializing, then those privileges could be removed, this is possible with pledge but not for traditional sandboxing wrappers.
In OpenBSD, most of the base userland have support for pledge, and more and more packaged software (including Chromium and Firefox) received some code to add pledge. If a program tries to use a system call that isn't in pledge promises list, it dies and the violation is reported in the system logs.
What makes pledge pretty cool is how it's easy to implement it in your software, it has a simple mechanism of system call families so you don't have to worry about listing every system calls, but only their categories (named promises), like reading a file, writing a file, executing binaries etc...
OpenBSD manual page for pledge(2)
2. Let's pledge a program §
I found a small utility that I will use to illustrate how to add pledge to a program. The program is qprint, a C quoted printable encoder/decoder. This kind of converter is quite easy to pledge because most of the time, they only take an input, do some computation and make an output, they don't run forever and don't do network.
qprint official project page
2.1. Digging in the sources §
When extracting the sources, we can find a bunch of files, we will focus at reading the *.c
files, the first thing we want to find is the function main()
.
It happens the main function is in the file qprint.c
. It's important to call pledge as soon as possible in the program, most of the time after variable initialization.
2.2. Modifying the code §
Adding pledge to a program requires to understand how it works, because some feature that aren't often used may be broken by pledge, and some programs having live reloading or being able to change behavior during runtime are complicated to pledge.
Within the function main
below variables declaration, We will add a call to pledge for stdio
because the program can display the result on the output, rpath
because it can read files and wpath
as it can also write files.
#include <unistd.h>
[...]
pledge("stdio rpath wpath", NULL);
It's ok, we imported the library providing pledge, and called it from within. But what if the pledge call fails for some reasons? We need to ensure it worked or abort the program. Let's add some checks.
#include <unistd.h>
#include <err.h>
[...]
if (pledge("stdio rpath wpath", NULL) == -1) {
err(1, "pledge call didn't work");
}
This is a lot better now, if pledge call failed, the program will stop and we will be warned about it. I don't know exactly under which circumstance it could fail, but maybe if promise name changes or doesn't exist anymore in a program, that would be bad if pledge silently failed.
2.3. Testing §
Now we made some changes to the program, we need to verify it's still working as expected.
Fortunately, qprint comes with a test suite which can be used with make wringer
, if the test suite pass and the tests have a good coverage, this mean we may have not break anything. If the test suite fails, we should have an error in the output of dmesg
telling us why it failed.
And, it failed!
qprint[98802]: pledge "cpath", syscall 5
This error (which killed the PID instantly) indicates that the pledge list is missing cpath
, this makes sense because it has to create new files if you specify an output file.
Adding cpath
to the list, and running the test suite again, all tests pass! Now, we exactly know that the software can't do anything except using the system calls we whitelisted.
We could tighten pledge more by dropping rpath
if the file is read from stdin, and cpath wpath
if the output is sent to stdout. I left this exercise to the reader :-)
2.4. The diff §
Here is my diff to add pledge support to qprint.
Index: qprint.c
--- qprint.c.orig
+++ qprint.c
@@ -2,6 +2,8 @@
#line 70 "./qprint.w"
#include "config.h"
+#include <unistd.h>
+#include <err.h>
#define REVDATE "16th December 2014" \
@@ -747,6 +749,9 @@ char*cp;
+if (pledge("stdio cpath rpath wpath", NULL) == -1) {
+ err(1, "pledge error");
+}
fi= stdin;
fo= stdout;
3. Using pledge in non-C programs §
It's actually possible to call pledge() in other programming languages, Perl has a library provided in OpenBSD base system that will work out of the box. For some other, such library may be packaged already (for python and Golang at least). If you use something less common, you can define an interface to call the library.
OpenBSD manual page for the Perl pledge library
Here is an example in Common LISP to create a new function c-kiosk-pledge
.
#+ecl
(progn
(ffi:clines "
#include <unistd.h>
void kioskPledge() {
pledge(\"dns inet stdio tty rpath\",NULL);
}
#endif")
#+openbsd
(ffi:def-function
("kioskPledge" c-kiosk-pledge)
() :returning :void))
It's possible to find which running programs are currently using pledge() by using ps auxww | awk '$8 ~ "p" { print }'
, any PID with a state containing p
indicates it's pledged.
If you want to add pledge to a packaged program on OpenBSD, make sure it still fully work.
Adding pledge to a program that contain most promises won't be doing much...
5. Exercise reader §
Now, if you want to practice, you can tighten the pledge calls to only allow qprint to use the pledge stdio
only in the case it's used in a pipe for input and output like this: ./qprint < input.txt > output.txt
.
Ideally, it should add the pledge cpath wpath
only when it writes into a file, and rpath
only when it has to read a file, so in the case of using stdin and stdout, only stdio
would have been added at the beginning.
Good luck, Have fun! Thanks to Brynet@ for the suggestion!
6. Conclusion §
The system call pledge() is a wonderful security feature that is reliable, and as it must be done in the source code, the program isn't run from within a sandboxed environment that may be possible to escape. I can't say pledge can't be escaped, but I think it's a lot less likely to be escaped than any other sandbox mechanism (especially since the program immediately dies if it tries to escape).
Next time, I'll present its companion system called unveil which is used to restrict access to the filesystem, except some developer defined files.