BCHS Logo

BCHS

why pledge(2)

… or, how I learned to love web application sandboxing

I use application-level sandboxing a lot because I make mistakes a lot; and when writing web applications, the price of making mistakes is very dear. In the early 2000s, that meant using systrace(4) on OpenBSD and NetBSD. Then it was seccomp(2) (followed by libseccomp(3)) on Linux. Then there was capsicum(4) on FreeBSD and sandbox_init(3) on Mac OS X.

All of these systems are invoked differently; and for the most part, whenever it came time to interface with one of them, I longed for sweet release from the nightmare. Please, try reading seccomp(2). To the end. Aligning web application logic and security policy would require an arduous (and usually trial-and-error or worse, copy-and-paste) process. If there was any process at all — if the burden of writing a policy didn't cause me to abandon sandboxing at the start.

And then there was pledge(2).

This document is about pledge(2) and why you should use it and love it. And why, in some respects, pledge(2) alone justifies the use of BCHS. If you know about sandboxing, you can skip the first part and jump to Why is it so bad? Or just tl;dr.

Introduction

A sandbox is a little bureaucrat sitting between your application and its system resources (files, network, etc.) that allows resource requests or denies them and (hopefully) kills the application. The rules the sandbox uses for this decision are called the policy. Sandboxes are incredibly useful: when initialised at the start of the application's lifetime, they define the resources required later in operation. So if an attacker has commandeered your application and is trying to acquire resources beyond the scope of your application's narrow specification — boom. (Or more commonly, if your policy lags behind your logic — boom.)

Sandbox logic in the kernel granting read(2) access to a file.
Sandbox disallowing read(2) access to a file, [hopefully] killing the requesting process.

To learn more in general about operating system sandboxes, my Bugs ex ante talk at EuroBSDCon 2014 may interest you. I won't talk about them much more. Obviously, they're a good thing: they allow an application developer to know exactly what the application should have access to.

The problem with sandboxing isn't the theory: it's the interface.

Sandbox Interfaces

Let's play a drinking game. The challenge is to stay out of the hospital.

  1. Navigate to seccomp(2).
  2. Read it to the end.
  3. Drink every time you don't understand.

For capsicum(4), the challenge is no less difficult. To see these in action, navigate no further than OpenSSH, which interfaces with these sandboxes: sandbox-seccomp-filter.c or sandbox-capsicum.c. (For a history lesson, you can even see sandbox-systrace.c.) Keep in mind that these do little more than restrict resources to open descriptors and the usual necessities of memory, signals, timing, etc. Keep that in mind and be horrified.

Fact is, writing sandbox policies is hard enough that (1) you're going to spend all of your time doing it, (2) you won't do it at all, or (3) you'll do it then scale it back to the point of only the most basic coverage.

Complexity is the worst enemy of security. Complex systems are hard to secure for an hours' worth of reasons, and this is especially true for computers and the internet. The internet is the most complex machine man has ever built by a lot, and it's hard to secure. Attackers have the advantage.

On web applications in particular — on any Internet-facing application — you need a sandbox. It's not just a local operator who can circumvent your protections. It's anybody on the other end of the pipe. So the question remains:

Why is it so bad?

The NSA's deliberate work to keep systems insecure. Capabilities systems like seccomp(2) and capsicum(4) need to be as flexible as possible to allow your application to sandbox any of the things it might [not] need to do. So every system call needs to be covered, and every argument to those system calls needs to be audited. Makes sense — in theory. But is this applicable in practise?

It's easy to forget that developers need to use sandboxes; and that, in fact, any sandbox interface will need to trade off complexity for usability. And if complexity is tied to thoroughness, then the trade-off is one of usability and rigour. It boils down to a classic problem: if your library is too complicated to use, people won't use it. And since security is often seen as an additional feature to any here today deprecated tomorrow web application, if your security interface is complicated and the benefits marginal relative to time of development, it will be left behind.

A further issue with Linux sandboxes in particular (seccomp(2) and friends) is due to the instability of the Linux ecosystem itself. Generic libc functions are implemented differently depending on whether you're using Alpine (musl) or Debian (glibc). This means that the same libc function may require different system calls. Moreover, the system calls available may differ — see the preprocessor checks in sandbox-seccomp-filter.c for different hardware architectures. What a nightmare!

And so: BCHS and pledge(2).

Why you should love security

In the argument of complexity and thoroughness, pledge(2) is a compromise. It acts like a series of usage profiles instead of focussing on the physical motions themselves. Much like one could simply say eating instead of raising food to mouth, masticating, swallowing, etc. If your utility needs to read a file (as in the above example), you don't need to white-list each system call involved in the reading of files: you enable a profile for reading files that includes fstat(2), fchown(2), etc. You can read more about it in Theo's a new mitigation mechanism (video).

Illustrating interface complexity with interface rigour. This is subjective, obviously.

For sake of argument, consider kcgi(3), which interfaces with all of these sandboxes. (Note: this was inspired by OpenSSH, but expanded for requiring more than just already-open pipes.) To begin, examine sandbox-seccom-filter.c and sandbox-capsicum.c. For sake of contrary argument, look also at the overly-coarse sandbox-darwin.c. And finally, sandbox-pledge.c.

Did you notice sandbox_init(3) in the lower-left corner? This is Mac OS X's simplistic interface to its own sandbox mechanism. Sadly, it barely warrants inclusion — not only is the implementation a mystery, it's also deprecated.

Does pledge(2) provide complete security coverage? No. And while the other, more extensive interfaces may do so, the argument isn't whether it theoretically may provide, but whether applications can use those capabilities. In other words, security must be practical.

To see a quick example of pledge(2) in a BCHS application, check out dblg.c. The pledge line is tiny: pledge("stdio rpath cpath wpath flock fattr", NULL). Any why even that complexity? The SQLite database in WAL mode, which requires the ability to create, read, write, and manipulate files. Without that, it could be whittled down even more. At a tutorial at AsiaBSDCon 2016, Secure CGI Applications in C on BSD, I gave even more examples.

tl;dr

For practical web applications, pledge(2) presents the best compromise of development simplicity and security coverage. This alone gives BCHS applications even more of a boost beyond the many other advantages of programming on OpenBSD. (E.g., and sufficient if not just necessary, manpages.)

Downsides? SQLite might require more file-system pledges than you're comfortable giving — especially in WAL mode. However, this is definitely something in the crosshairs of ksql(3): forking a process, like kcgi(3) does, that handles the database I/O and communicates with the master over pipes.

Disclaimer: I wrote kcgi(3) and ksql(3), both of which are mentioned several times. I'd love to mention other tools that do the same thing, but they're not there.