Make pledge() and unveil() work amazingly

This change reconciles our pledge() implementation with the OpenBSD
kernel source code. We now a polyfill that's much closer to OpenBSD's
behavior. For example, it was discovered that "stdio" permits threads.
There were a bunch of Linux system calls that needed to be added, like
sched_yield(). The exec / execnative category division is now dropped.
We're instead using OpenBSD's "prot_exec" promise for launching APE
binaries and dynamic shared objects. We also now filter clone() flags.

The pledge.com command has been greatly improved. It now does unveiling
by default when Landlock is available. It's now smart enough to unveil a
superset of paths that OpenBSD automatically unveils with pledge(), such
as /etc/localtime. pledge.com also now checks if the executable being
launched is a dynamic shared object, in which case it unveils libraries.

These changes now make it possible to pledge curl on ubuntu 20.04 glibc:

    pledge.com -p 'stdio rpath prot_exec inet dns tty sendfd recvfd' \
        curl -s https://justine.lol/hello.txt

Here's what pledging curl on Alpine 3.16 with Musl Libc looks like:

    pledge.com -p 'stdio rpath prot_exec dns inet' \
        curl -s https://justine.lol/hello.txt

Here's what pledging curl.com w/ ape loader looks like:

    pledge.com -p 'stdio rpath prot_exec dns inet' \
        o//examples/curl.com https://justine.lol/hello.txt

The most secure sandbox, is curl.com converted to static ELF:

    o//tool/build/assimilate.com o//examples/curl.com
    pledge.com -p 'stdio rpath dns inet' \
        o//examples/curl.com https://justine.lol/hello.txt

A weird corner case needed to be handled when resolving symbolic links
during the unveiling process, that's arguably a Landlock bug. It's not
surprising since Musl and Glibc are also inconsistent here too.
This commit is contained in:
Justine Tunney 2022-07-19 21:18:33 -07:00
parent 92cb144fff
commit 98254a7c1f
28 changed files with 934 additions and 292 deletions

View file

@ -32,9 +32,11 @@
#include "libc/macros.internal.h"
#include "libc/mem/io.h"
#include "libc/mem/mem.h"
#include "libc/runtime/internal.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/sock.h"
#include "libc/sock/struct/sockaddr.h"
#include "libc/stdio/stdio.h"
#include "libc/sysv/consts/af.h"
#include "libc/sysv/consts/at.h"
#include "libc/sysv/consts/f.h"
@ -47,8 +49,10 @@
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/sock.h"
#include "libc/testlib/ezbench.h"
#include "libc/testlib/testlib.h"
#include "libc/thread/spawn.h"
#include "libc/time/time.h"
STATIC_YOINK("zip_uri_support");
@ -58,6 +62,8 @@ void OnSig(int sig) {
// do nothing
}
int memfd_secret(unsigned int); // our ENOSYS threshold
int extract(const char *from, const char *to, int mode) {
int fdin, fdout;
if ((fdin = open(from, O_RDONLY)) == -1) return -1;
@ -306,18 +312,17 @@ TEST(pledge, mmap) {
EXPECT_TRUE(WIFEXITED(ws) && !WEXITSTATUS(ws));
}
TEST(pledge, mmapExec) {
TEST(pledge, mmapProtExec) {
if (IsOpenbsd()) return; // b/c testing linux bpf
char *p;
int ws, pid;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio exec", "stdio"));
ASSERT_SYS(0, 0, pledge("stdio prot_exec", 0));
ASSERT_NE(MAP_FAILED, (p = mmap(0, FRAMESIZE, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0)));
ASSERT_SYS(0, 0, mprotect(p, FRAMESIZE, PROT_READ));
ASSERT_SYS(EPERM, MAP_FAILED,
mprotect(p, FRAMESIZE, PROT_READ | PROT_EXEC));
ASSERT_SYS(0, 0, mprotect(p, FRAMESIZE, PROT_READ | PROT_EXEC));
ASSERT_NE(MAP_FAILED, mmap(0, FRAMESIZE, PROT_EXEC | PROT_READ,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0));
_Exit(0);
@ -440,7 +445,7 @@ TEST(pledge, execpromises_ok) {
struct stat st;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio execnative", "stdio"));
ASSERT_SYS(0, 0, pledge("stdio exec", "stdio"));
execl("life.elf", "life.elf", 0);
_Exit(127);
}
@ -455,7 +460,7 @@ TEST(pledge, execpromises_notok) {
struct stat st;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio execnative", "stdio"));
ASSERT_SYS(0, 0, pledge("stdio exec", "stdio"));
execl("sock.elf", "sock.elf", 0);
_Exit(127);
}
@ -470,7 +475,7 @@ TEST(pledge, execpromises_reducesAtExecOnLinux) {
struct stat st;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio inet tty execnative", "stdio tty"));
ASSERT_SYS(0, 0, pledge("stdio inet tty exec", "stdio tty"));
execl("sock.elf", "sock.elf", 0);
_Exit(127);
}
@ -485,7 +490,7 @@ TEST(pledge_openbsd, execpromisesIsNull_letsItDoAnything) {
struct stat st;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio execnative", 0));
ASSERT_SYS(0, 0, pledge("stdio exec", 0));
execl("sock.elf", "sock.elf", 0);
_Exit(127);
}
@ -500,7 +505,7 @@ TEST(pledge_openbsd, execpromisesIsSuperset_letsItDoAnything) {
struct stat st;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio rpath execnative", "stdio rpath tty inet"));
ASSERT_SYS(0, 0, pledge("stdio rpath exec", "stdio rpath tty inet"));
execl("sock.elf", "sock.elf", 0);
_Exit(127);
}
@ -511,7 +516,7 @@ TEST(pledge_openbsd, execpromisesIsSuperset_letsItDoAnything) {
TEST(pledge_linux, execpromisesIsSuperset_notPossible) {
if (IsOpenbsd()) return;
ASSERT_SYS(EINVAL, -1, pledge("stdio execnative", "stdio inet execnative"));
ASSERT_SYS(EINVAL, -1, pledge("stdio exec", "stdio inet exec"));
}
TEST(pledge_openbsd, execpromises_notok) {
@ -520,7 +525,7 @@ TEST(pledge_openbsd, execpromises_notok) {
struct stat st;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio execnative", "stdio"));
ASSERT_SYS(0, 0, pledge("stdio exec", "stdio"));
execl("sock.elf", "sock.elf", 0);
_Exit(127);
}
@ -528,3 +533,51 @@ TEST(pledge_openbsd, execpromises_notok) {
EXPECT_TRUE(WIFSIGNALED(ws));
EXPECT_EQ(SIGABRT, WTERMSIG(ws));
}
TEST(pledge_openbsd, bigSyscalls) {
if (IsOpenbsd()) return; // testing lunix
int ws, pid;
struct stat st;
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
ASSERT_SYS(0, 0, pledge("stdio", 0));
ASSERT_SYS(ENOSYS, -1, memfd_secret(0));
ASSERT_SYS(ENOSYS, -1, sys_bogus());
_Exit(0);
}
EXPECT_NE(-1, wait(&ws));
EXPECT_TRUE(WIFEXITED(ws));
EXPECT_EQ(0, WEXITSTATUS(ws));
}
int LockWorker(void *arg, int tid) {
flockfile(stdout);
ASSERT_EQ(gettid(), stdout->lock.lock);
funlockfile(stdout);
return 0;
}
TEST(pledge, threadWithLocks_canCodeMorph) {
struct spawn worker;
int ws, pid;
// not sure how this works on OpenBSD but it works!
if (!fork()) {
ASSERT_SYS(0, 0, pledge("stdio prot_exec", 0));
ASSERT_SYS(0, 0, _spawn(LockWorker, 0, &worker));
ASSERT_SYS(0, 0, _join(&worker));
_Exit(0);
}
EXPECT_NE(-1, wait(&ws));
EXPECT_TRUE(WIFEXITED(ws));
EXPECT_EQ(0, WEXITSTATUS(ws));
}
BENCH(pledge, bench) {
int pid;
if (!fork()) {
ASSERT_SYS(0, 0, pledge("stdio", 0));
EZBENCH2("sched_yield", donothing, sched_yield());
_Exit(0);
}
wait(0);
}

View file

@ -273,3 +273,23 @@ TEST(unveil, usedTwice_forbidden_worksWithPledge) {
}
EXPECT_SYS(0, 0, munmap(gotsome, FRAMESIZE));
}
TEST(unveil, lotsOfPaths) {
int i, n;
SPAWN();
n = 100;
for (i = 0; i < n; ++i) {
ASSERT_SYS(0, 0, touch(xasprintf("%d", i), 0644));
ASSERT_SYS(0, 0, touch(xasprintf("%d-", i), 0644));
}
for (i = 0; i < n; ++i) {
ASSERT_SYS(0, 0, unveil(xasprintf("%d", i), "rw"));
}
ASSERT_SYS(0, 0, unveil(0, 0));
for (i = 0; i < n; ++i) {
ASSERT_SYS(0, 3, open(xasprintf("%d", i), O_RDONLY));
ASSERT_SYS(0, 0, close(3));
ASSERT_SYS(EACCES_OR_ENOENT, -1, open(xasprintf("%d-", i), O_RDONLY));
}
EXITS(0);
}