July 13th, 2022 @ justine’s web page
OpenBSD is an operating system that’s famous for its focus on security.
Unfortunately, OpenBSD leader Theo states that there
are only 7000
users of OpenBSD. So it’s a very small but elite group, that wields
a disproportionate influence; since we hear all the time about the
awesome security features these guys get to use, even though we usually
can’t use them ourselves.
Pledge is like the forbidden fruit we all covet when the boss says we
must use things like Linux. Why does it matter? It’s because pledge()
actually makes security comprehensible. Linux has never really had a
security layer that mere mortals can understand. For example, let’s say
you want to do something on Linux like control whether or not some
program you downloaded from the web is allowed to have telemetry. You’d
need to write stuff like this:
static const struct sock_filter kFilter[] = { /* L0*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, syscall, 0, 14 - 1), /* L1*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[0])), /* L2*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 2, 4 - 3, 0), /* L3*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 10, 0, 13 - 4), /* L4*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[1])), /* L5*/ BPF_STMT(BPF_ALU | BPF_AND | BPF_K, ~0x80800), /* L6*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 1, 8 - 7, 0), /* L7*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 2, 0, 13 - 8), /* L8*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(args[2])), /* L9*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0, 12 - 10, 0), /*L10*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 6, 12 - 11, 0), /*L11*/ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 17, 0, 13 - 11), /*L12*/ BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), /*L13*/ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(nr)), /*L14*/ /* next filter */ };
Oh my gosh. It’s like we traded one form of security privilege for
another. OpenBSD limits security to a small pond, but makes it easy.
Linux is a big tent, but makes it impossibly hard. SECCOMP BPF might as
well be the Traditional Chinese of programming languages, since only a
small number of people who’ve devoted the oodles of time it takes to
understand code like what you see above have actually been able to
benefit from it. But if you’ve got OpenBSD privilege, then doing the
same thing becomes easy:
pledge("stdio rpath", 0);
That’s really all OpenBSD users have to do to prevent things like leaks
of confidential information. So how do we get it that simple on Linux? I
believe the answer is to find someone with enough free time to figure
out how to use SECCOMP BPF to implement pledge. The latest volunteer is
me, so look upon my code ye mighty and despair.
- cosmopolitan/libc/calls/pledge.c
system call - cosmopolitan/libc/calls/pledge-linux.c
system call polyfill - cosmopolitan/tool/build/pledge.c
pledge command - cosmopolitan/test/libc/calls/pledge_test.c
unit tests
There’s been a few devs in the past who’ve tried this. I’m not going to
name names, because most of these projects were never completed. When it
comes to SECCOMP, the online tutorials only explain how to whitelist the
system calls themselves, so most people lose interest before figuring
out how to filter arguments. The projects that got further along also
had oversights like allowing the changing of setuid/setgid/sticky bits.
So none of the current alternatives should be used. I believe this
effort gets us much closer to having pledge() than ever before.
Command Line Utility
![Linux [Linux]](https://hacktech.info/wp-content/plugins/trx_addons/components/lazy-load/images/placeholder.png)
I originally wrote my pledge() polyfill for
the redbean web server as a
sandboxing solution. However it turns out pledge() is robust enough as
an abstraction that I thought it’d be useful to create a small command
line utility which launches processes under pledge(), so that anyone can
use it, without having to configure it in C code.
pledge-1.8.com
88kb – x86-64 elf executable (debug data, source code)
Written by Justine Alexandra Roberts Tunney (Twitter, GitHub, LinkedIn)
22d33574e244883a87e54169f4ed82ea40cabb17b79c9e57559b0fa8454dd698
That binary will work on all Linux distros since RHEL6. Root privileges
are not required. You just use it to wrap your command invocations. It’s
so tiny and lightweight that it only adds a few microseconds of startup
latency to your program. It’s great for shell scripts and automated
tools. For example, if you want to run the list directory command, and
only permit that command to do basic stdio (-p stdio
) and
filesystem path (-p rpath
) reading in the current directory
(-v .
), then you’d say:
$ wget https://justine.lol/pledge/pledge.com
$ chmod +x pledge.com
$ ./pledge.com -v. -p 'stdio rpath' ls
file listing output...
You can now be certain your ls command isn’t doing things like spying on
you, or uploading your bitcoin wallet to the cloud. However let’s say
authorizing network access is what you want. One command that has a real
legitimate need for that is curl. However, since it needs needs DNS,
it’s a little trickier because DNS is the Hunger Games of systems
engineering, and not all Libc implementations agree on how it should be
implemented. Here’s some strategies depending on your tools and distro:
# standard curl on alpine linux 3.16 (musl) ./pledge.com -p 'stdio rpath dns inet' curl -s http://justine.lol/hello.txt # standard curl on ubuntu 22.04 (glibc) ./pledge.com -p 'stdio rpath inet dns tty sendfd recvfd' curl -s http://justine.lol/hello.txt hello world # cosmopolitan's curl as static binary # see git clone and make instructions below ./assimilate.com ./curl.com ./pledge.com -p 'stdio rpath dns inet' ./curl.com https://justine.lol/hello.txt # cosmopolitan's curl as ape binary # non-assimilated cosmopolitan ape binary ./pledge.com -p 'stdio rpath prot_exec dns inet' ./curl.com https://justine.lol/hello.txt
The choice of C library usually impacts which permissions are needed.
Musl and Cosmopolitan need the least permission since they were built
with sandboxing in mind. Glibc on the other hand does some strange stuff
with DNS, which requires us to weaken the sandbox with recvmsg() and
sendmsg() which also enable SCM_RIGHTS unfortunately.
Both Musl and Glibc use dynamic binaries. In order to be able to launch
them, pledge.com temporarily implies both exec
and
prot_exec
. We then inject an
LD_PRELOAD
library which runs inside the process at
initialization. That library calls pledge() again automatically, and
drops the both exec
and prot_exec
privileges
if needed. This dynamic library also lets us print helpful messages to
stderr to explain which promises are needed when a violation occurs.
Let’s say you have a public ssh server and you want to let people read
and take notes of your book collection, but you don’t want anyone
rewriting your books. In that case, you can repupose something like the
nano command as a strictly read-only editor. Since nano has a TUI
interface, you’d need to grant it TTY privileges.
./pledge.com -v $HOME/books -np 'stdio rpath tty' nano ~/books/bofh.txt
Here’s how you’d sandbox Vim to only be able to change the current
directory, tested on Alpine and Ubuntu.
./pledge.com -v rwc:. -v /etc/vim -v $HOME/.vimrc -v /usr{,/local}/share/vim -p 'stdio rpath wpath cpath tty prot_exec' vim
Here’s how you’d sandbox Emacs to only be able to change the current
directory, tested on Alpine and Ubuntu.
./pledge.com -v rwc:. -v $HOME/.emacs -v rwc:$HOME/.emacs.d -v /etc/emacs -v /etc/passwd -v /usr/share/X11/locale -v /usr{,/local}/{libexec,share}/emacs -p 'stdio rpath wpath cpath tty proc tmppath prot_exec' emacs -nw
Troubleshooting
If your program crashes, then you can figure out why by tracing the
binary and seeing which system call is EPERM’ing or which veiled path is
EACCES’ing. For example, let’s see what happens if we reduce the
privileges to just stdio.
$ strace -ff ./pledge.com -p stdio ls
open("/etc/ld-musl-x86_64.path", O_RDONLY|O_CLOEXEC) = -1 EPERM (Operation not permitted)
Well that didn’t take long. Now that you know what’s wrong, you would
then consult the Promises section to see which
promise you need. For example, you’d know open(O_RDONLY)
is
provided by rpath
and that in order to fork()
you need -p proc
.
Resource Limits
In addition to polyfilling pledge, your pledge command is also able to
apply some other very important safety hacks that aren’t obvious to the
uninitiated. For example, we’ve all run a program before that hammers
the system. Linux is very generous in how much memory programs can
allocate. An accidental loop in just one program, by default on Linux,
will absolutely take the whole machine out of commission for a few
minutes before the “OOM Killer” kicks in. In other cases, like a fork()
bomb, the default Linux environment provides no such protection, so it’s
essentially equivalent to a blue screen of death.
Your pledge command imposes some perfectly reasonable resource quotas on
programs by default, to prevent that from happening. By default, unless
you tune the flags, a program is allowed to use only the amount of
memory you have. If you’ve permitted it to fork off new processes, then
it won’t be able to spawn more of them at the same time than twice your
number of CPUs. This way if your sandboxed program gets out of control,
it’ll most likely crash itself before it can crash your whole computer.
We also have a niceness feature. Have you ever had a program use so much
disk i/o that everything crawls to a halt? You run some program, and
then suddenly every small file takes seconds to load in Emacs? Your
pledge command can fix that. If you’re got a compute heavy long running
program, then pass the -n
flag for a nice
that’s actually nice. The naive nice command doesn’t really do much,
since it doesn’t change the scheduler and it doesn’t change the i/o
priority. This command actually does. Using the -n
flag
will guarantee the sandbox program will stay out of the way, since the
kernel will only let it use spare capacity.
Pledge Command Flags
- -n
- Apply maximum niceness to program. This means
- nice is set to 19,
- i/o priority is set to idle, and
- scheduler is set to idle.
- -p PROMISES
- Defaults to
-p 'stdio rpath'
. It’s repeatable. May
contain any of following separated by spaces:
See also
the Promises section below which goes into
much greater depth on what each category does.- stdio: allow stdio, threads, and benign system calls
- rpath: read-only path ops
- wpath: write path ops
- cpath: create path ops
- dpath: create special files
- flock: allow file l