I spent an hour or two trying to make the Lagrange browser more secure on OpenBSD by using the pledge system call. For those who don't know, pledge is a system call that allows a process to restrict the system calls it can make. This vastly reduces the attack surface of the process, and makes it much harder for an attacker to do anything malicious if they manage to exploit the process. Try popping a shell when execve is disabled!
OpenBSD man page for pledge(2)
A simple example. Pledge is called with a string that lists categories of system calls that the process is allowed to make. Most UNIX tools are single purpose, so there is no reason for `sed` to open a network socket, Nor for `xxd` to invoke a new command. Usually pledge() is called after the application has done all its setup and is ready to start processing data. Since initialization is usually less exploitabale and process is where the majority of the work is done, it makes sense to restrict the process at this point.
The pledge `stdio rpath` allows the process to read to files and make basic I/O and memory operations. Thus the `open` call later on with `O_RDONLY` will work.
pledge("stdio rpath", NULL); int fd = open("file.txt", O_RDONLY);
However, if for some reason there is a bug and it tried to open a file for writing, the process gets SIGABRT and terminates.
pledge("stdio rpath", NULL); // OpenBSD will kill the process here int fd = open("file.txt", O_WRONLY);
Lagrange is written in C and uses the SDL library for the GUI, which in turn uses OpenGL for rendering. Took me five minutes to figure out where Lagrange finished initializing and I can start pledge() (It's in `run_App()` after `init_App_()` called in `src/app.c`).
I started with a small set of pledge promises and use `ktrace` to see what system calls where needed. And adds missing promises until the blocked systemcall changes. However, I quickly ran into a problem: No matter which promise I add, OpenGL alwasys fails to allocate memory and create a texture.
Looking in LLDB shows the following trace.
(lldb) bt
After asking on Reddit. An OpenBSD devleoper pointed out that it might need some specific promises. Maybe `drm` which is not fully documented.
And that works! With more work I made a patch that pledges Lagrange and still keeping every functionality working.
diff --git a/src/app.c b/src/app.c index ee1782cd..40cf863f 100644 --- a/src/app.c +++ b/src/app.c @@ -94,6 +94,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ # include <SDL_misc.h> #endif +#if defined (__OpenBSD__) +# include <unistd.h> +#endif + iDeclareType(App) #if defined (iPlatformAppleDesktop) @@ -2590,6 +2594,12 @@ iBool schemeProxyHostAndPort_App(iRangecc scheme, const iString **host, uint16_t int run_App(int argc, char **argv) { init_App_(&app_, argc, argv); +#if defined (__OpenBSD__) + if(pledge("stdio wpath cpath rpath unix inet drm sendfd dns", NULL) != 0) { + fprintf(stderr, "[pledge] Failed to lock down Lagrange with pledge\n"); + return -1; + } +#endif const int rc = run_App_(&app_); deinit_App(&app_); return rc;
Fun thing to know, despite the OpenBSD documentation. The `video` and `audio` promises are _not_ what they seem to be. `audio` allows direct access to the audio device. Likewise, `video` is about access to UVC devices. However, most applications talk to the sndio audio server and you use DRM for rendering. What you actually need for audio is `unix sendfd` and for video `drm`.
I'm thinking if I should add unveil too. Both Chromium and Firefox are unveiled on OpenBSD to only allow RW access to their cache, config, data directories and the download folder. The decision was conterversial and some hate it.
Not sure, don't know. I am unsure if I want to upstream the pledge patch to Lagrange. Future dependencies might need more promises and I don't want to be the one maintaining the list. If you want the patch, consider it licensed under the same license as Lagrange. With the pledge, it should be impossible to escilate to root or make command calls. However it is still possible to read files and directories. Including everything in the home directory.
Someone mentioned on my thread on Reddit that pledge may not be super helpful. My pledge promieses are missing `proc exec` so Lagrange won't be able to launch other applications to open files it can't read. But adding `proc exec` also makes the promises too broad that the pledge doesn't really protect anything that an attacker can do.
This is my revised patch that also unveils so only the configuration and download directory is accessible.
diff --git a/src/app.c b/src/app.c index ee1782cd..33f289eb 100644 --- a/src/app.c +++ b/src/app.c @@ -94,6 +94,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ # include <SDL_misc.h> #endif +#if defined (__OpenBSD__) +# include <unistd.h> +#endif + iDeclareType(App) #if defined (iPlatformAppleDesktop) @@ -2588,8 +2592,31 @@ iBool schemeProxyHostAndPort_App(iRangecc scheme, const iString **host, uint16_t return iTrue; } +#if defined (__OpenBSD__) +static void unveil_under_home(const char* relpath, const char* priv) +{ + char* home = getenv("HOME"); + if(home == NULL) { + fprintf(stderr, "$HOME not set. Cannot unveil\n"); + } + char* p = malloc(strlen(relpath) + strlen(priv) + 1); + strcpy(p, relpath); + strcat(p, priv); + free(p); +} +#endif int run_App(int argc, char **argv) { init_App_(&app_, argc, argv); +#if defined (__OpenBSD__) + unveil_under_home(".config/lagrange", "rwc"); + unveil_under_home("Downloads", "rwc"); + unveil(NULL, NULL); + + if(pledge("stdio wpath cpath rpath unix inet drm sendfd dns", NULL) != 0) { + fprintf(stderr, "[pledge] Failed to lock down Lagrange with pledge\n"); + return -1; + } +#endif const int rc = run_App_(&app_); deinit_App(&app_); return rc;