Pledging OpenGL applications on OpenBSD

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!

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
* thread #1, stop reason = signal SIGABRT
  * frame #0: 0x000000e8abb8563b libc.so.99.0`_thread_sys_futex at -:2
    frame #1: 0x000000e8ddc9f31f radeonsi_dri.so`___lldb_unnamed_symbol11945 + 63
    frame #2: 0x000000e8de36077f radeonsi_dri.so`___lldb_unnamed_symbol25811 + 767
    frame #3: 0x000000e8de1a05bf radeonsi_dri.so`___lldb_unnamed_symbol23565 + 31
    frame #4: 0x000000e8de3803ae radeonsi_dri.so`___lldb_unnamed_symbol26002 + 1982
    frame #5: 0x000000e8de37caf5 radeonsi_dri.so`___lldb_unnamed_symbol25974 + 1397
    frame #6: 0x000000e8dd85e5b4 radeonsi_dri.so`___lldb_unnamed_symbol2481 + 180
    frame #7: 0x000000e8dd884289 radeonsi_dri.so`___lldb_unnamed_symbol2974 + 889
    frame #8: 0x000000e8dd883c32 radeonsi_dri.so`___lldb_unnamed_symbol2972 + 306
    frame #9: 0x000000e8dd885a66 radeonsi_dri.so`___lldb_unnamed_symbol2979 + 102
    frame #10: 0x000000e8dd8ae336 radeonsi_dri.so`___lldb_unnamed_symbol3289 + 4406
    frame #11: 0x000000e8dd8af9fe radeonsi_dri.so`___lldb_unnamed_symbol3292 + 110
    frame #12: 0x000000e919d50249 libSDL2.so.0.15`GL_CreateTexture + 2425
    frame #13: 0x000000e919d442f7 libSDL2.so.0.15`SDL_CreateTexture_REAL + 775
    frame #14: 0x000000e919d44246 libSDL2.so.0.15`SDL_CreateTexture_REAL + 598
    frame #15: 0x000000e6765405e3 lagrange`initCache_StbText_(d=0x000000e9299bb700) at text_stb.c:419:16
    frame #16: 0x000000e676540d62 lagrange`resetCache_StbText_(d=0x000000e9299bb700) at text_stb.c:498:5
    frame #17: 0x000000e676540cd8 lagrange`resetFontCache_Text(d=0x000000e9299bb700) at text_stb.c:515:5
    frame #18: 0x000000e67649143b lagrange`invalidate_Window_(d=0x000000e903852900, forced=true) at window.c:909:9
    frame #19: 0x000000e67651564a lagrange`invalidate_MainWindow_(d=0x000000e903852900, forced=true) at window.c:919:5
    frame #20: 0x000000e6764921ab lagrange`handleWindowEvent_MainWindow_(d=0x000000e903852900, ev=0x00007681a6505158) at window.c:1208:13
    frame #21: 0x000000e67649175c lagrange`processEvent_Window(d=0x000000e903852900, ev=0x00007681a6505158) at window.c:1305:24
    frame #22: 0x000000e6763acc5c lagrange`processEvents_App(eventMode=waitForNewEvents_AppEventMode) at app.c:2183:35
    frame #23: 0x000000e6763b6ca8 lagrange`run_App_(d=0x000000e6765b0e98) at app.c:2427:9
    frame #24: 0x000000e6763b5980 lagrange`run_App(argc=1, argv=0x00007681a6505298) at app.c:2603:20
    frame #25: 0x000000e6763a9f54 lagrange`main(argc=1, argv=0x00007681a6505298) at main.c:96:5
    frame #26: 0x000000e6763a9c7b lagrange`__start + 299

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.

Edit

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.

Edit 2

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;
Author's profile. Photo taken in VRChat by my friend Tast+
Martin Chang
Systems software, HPC, GPGPU and AI. I mostly write stupid C++ code. Sometimes does AI research. Chronic VRChat addict

I run TLGS, a major search engine on Gemini. Used by Buran by default.


  • marty1885 \at protonmail.com
  • Matrix: @clehaxze:matrix.clehaxze.tw
  • Jami: a72b62ac04a958ca57739247aa1ed4fe0d11d2df