cosmopolitan/test/libc/calls/unveil_test.c

421 lines
14 KiB
C
Raw Normal View History

2022-07-18 09:11:06 +00:00
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi
Copyright 2022 Justine Alexandra Roberts Tunney
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/calls.h"
#include "libc/calls/landlock.h"
#include "libc/calls/struct/dirent.h"
#include "libc/calls/struct/stat.h"
2022-07-25 05:34:13 +00:00
#include "libc/calls/syscall-sysv.internal.h"
2022-07-18 09:11:06 +00:00
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/intrin/kprintf.h"
#include "libc/mem/io.h"
#include "libc/runtime/gc.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/sock.h"
2022-07-18 09:11:06 +00:00
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/af.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/msync.h"
2022-07-18 09:11:06 +00:00
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"
2022-07-18 09:11:06 +00:00
#include "libc/sysv/consts/s.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/sock.h"
2022-07-18 09:11:06 +00:00
#include "libc/testlib/testlib.h"
#include "libc/thread/spawn.h"
2022-07-18 09:11:06 +00:00
#include "libc/x/x.h"
STATIC_YOINK("zip_uri_support");
#define EACCES_OR_ENOENT (IsOpenbsd() ? ENOENT : EACCES)
#define SPAWN(METHOD) \
{ \
int ws, pid; \
ASSERT_NE(-1, (pid = METHOD())); \
2022-07-18 09:11:06 +00:00
if (!pid) {
#define EXITS(rc) \
_Exit(0); \
} \
ASSERT_NE(-1, wait(&ws)); \
ASSERT_TRUE(WIFEXITED(ws)); \
ASSERT_EQ(rc, WEXITSTATUS(ws)); \
}
char testlib_enable_tmp_setup_teardown;
struct stat st;
static bool SupportsLandlock(void) {
int e = errno;
bool r = landlock_create_ruleset(0, 0, LANDLOCK_CREATE_RULESET_VERSION) >= 0;
errno = e;
return r;
}
2022-07-25 05:34:13 +00:00
void SetUpOnce(void) {
2022-07-18 09:11:06 +00:00
if (!(IsLinux() && SupportsLandlock()) && !IsOpenbsd()) exit(0);
}
void SetUp(void) {
// make sure zipos maps executable into memory early
ASSERT_SYS(0, 0, stat("/zip/life.elf", &st));
}
int extract(const char *from, const char *to, int mode) {
int fdin, fdout;
if ((fdin = open(from, O_RDONLY)) == -1) return -1;
if ((fdout = creat(to, mode)) == -1) {
close(fdin);
return -1;
}
if (_copyfd(fdin, fdout, -1) == -1) {
close(fdout);
close(fdin);
return -1;
}
return close(fdout) | close(fdin);
}
TEST(unveil, api_differences) {
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, stat("/", &st));
ASSERT_SYS(0, 0, unveil(".", "rw"));
if (IsOpenbsd()) {
// openbsd imposes restrictions immediately
ASSERT_SYS(ENOENT, -1, open("/", O_RDONLY | O_DIRECTORY));
} else {
// restrictions on linux don't go into effect until unveil(0,0)
ASSERT_SYS(0, 3, open("/", O_RDONLY | O_DIRECTORY));
ASSERT_SYS(0, 0, close(3));
}
ASSERT_SYS(0, 0, unveil(0, 0));
// error numbers are inconsistent
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("/", O_RDONLY | O_DIRECTORY));
// wut
if (IsLinux()) {
ASSERT_SYS(0, 3, open("/", O_PATH)); // wut
ASSERT_SYS(0, 0, stat("/", &st)); // wut
}
EXITS(0);
}
TEST(unveil, rx_readOnlyPreexistingExecutable_worksFine) {
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, mkdir("folder", 0755));
ASSERT_SYS(0, 0, extract("/zip/life.elf", "folder/life.elf", 0755));
ASSERT_SYS(0, 0, unveil("folder", "rx"));
ASSERT_SYS(0, 0, unveil(0, 0));
SPAWN(fork);
execl("folder/life.elf", "folder/life.elf", 0);
kprintf("execve failed! %s\n", strerror(errno));
_Exit(127);
2022-07-18 09:11:06 +00:00
EXITS(42);
EXITS(0);
}
TEST(unveil, r_noExecutePreexistingExecutable_raisesEacces) {
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, mkdir("folder", 0755));
ASSERT_SYS(0, 0, extract("/zip/life.elf", "folder/life.elf", 0755));
ASSERT_SYS(0, 0, unveil("folder", "r"));
ASSERT_SYS(0, 0, unveil(0, 0));
SPAWN(fork);
ASSERT_SYS(EACCES, -1, execl("folder/life.elf", "folder/life.elf", 0));
2022-07-18 09:11:06 +00:00
EXITS(0);
EXITS(0);
}
TEST(unveil, canBeUsedAgainAfterVfork) {
ASSERT_SYS(0, 0, touch("bad", 0644));
ASSERT_SYS(0, 0, touch("good", 0644));
SPAWN(fork);
SPAWN(vfork);
ASSERT_SYS(0, 0, unveil("bad", "r"));
ASSERT_SYS(0, 0, unveil("good", "r"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 3, open("bad", 0));
EXITS(0);
ASSERT_SYS(0, 0, unveil("good", "r"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 3, open("good", 0));
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("bad", 0));
EXITS(0);
}
2022-07-18 09:11:06 +00:00
TEST(unveil, rwc_createExecutableFile_isAllowedButCantBeRun) {
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, mkdir("folder", 0755));
ASSERT_SYS(0, 0, unveil("folder", "rwc"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 0, extract("/zip/life.elf", "folder/life.elf", 0755));
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, stat("folder/life.elf", &st));
ASSERT_SYS(EACCES, -1, execl("folder/life.elf", "folder/life.elf", 0));
2022-07-18 09:11:06 +00:00
EXITS(0);
EXITS(0);
}
TEST(unveil, rwcx_createExecutableFile_canAlsoBeRun) {
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, mkdir("folder", 0755));
ASSERT_SYS(0, 0, unveil("folder", "rwcx"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 0, extract("/zip/life.elf", "folder/life.elf", 0755));
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, stat("folder/life.elf", &st));
execl("folder/life.elf", "folder/life.elf", 0);
kprintf("execve failed! %s\n", strerror(errno));
_Exit(127);
2022-07-18 09:11:06 +00:00
EXITS(42);
EXITS(0);
}
TEST(unveil, dirfdHacking_doesntWork) {
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, touch("garden/secret.txt", 0644));
ASSERT_SYS(0, 3, open("garden", O_RDONLY | O_DIRECTORY));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(EACCES_OR_ENOENT, -1, openat(3, "secret.txt", O_RDONLY));
EXITS(0);
}
TEST(unveil, mostRestrictivePolicy) {
if (IsOpenbsd()) return; // openbsd behaves oddly; see docs
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, touch("garden/secret.txt", 0644));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("jail", O_RDONLY | O_DIRECTORY));
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
EXITS(0);
}
2022-07-18 09:11:06 +00:00
TEST(unveil, overlappingDirectories_inconsistentBehavior) {
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, makedirs("f1/f2", 0755));
ASSERT_SYS(0, 0, extract("/zip/life.elf", "f1/f2/life.elf", 0755));
ASSERT_SYS(0, 0, unveil("f1", "x"));
ASSERT_SYS(0, 0, unveil("f1/f2", "r"));
ASSERT_SYS(0, 0, unveil(0, 0));
if (IsOpenbsd()) {
// OpenBSD favors the most restrictive policy
SPAWN(fork);
2022-07-18 09:11:06 +00:00
ASSERT_SYS(0, 0, stat("f1/f2/life.elf", &st));
ASSERT_SYS(EACCES, -1, execl("f1/f2/life.elf", "f1/f2/life.elf", 0));
2022-07-18 09:11:06 +00:00
EXITS(0);
} else {
// Landlock (Linux) uses the union of policies
//
// TODO(jart): this test flakes on github actions. it reports an
// exit code of 0! find out why this is happening...
// so far it's happened to MODE=rel and MODE=tiny...
//
// SPAWN(fork);
// ASSERT_SYS(0, 0, stat("f1/f2/life.elf", &st));
// execl("f1/f2/life.elf", "f1/f2/life.elf", 0);
// kprintf("execve failed! %s\n", strerror(errno));
// _Exit(127);
// EXITS(42);
2022-07-18 09:11:06 +00:00
}
EXITS(0);
}
TEST(unveil, usedTwice_allowedOnLinux) {
if (IsOpenbsd()) return;
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, xbarf("jail/ok.txt", "hello", 5));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, xbarf("garden/secret.txt", "hello", 5));
ASSERT_SYS(0, 0, mkdir("heaven", 0755));
ASSERT_SYS(0, 0, xbarf("heaven/verysecret.txt", "hello", 5));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil("garden", "rw"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 3, open("garden/secret.txt", O_RDONLY));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil("heaven", "rw")); // not allowed, superset of parent
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 4, open("jail/ok.txt", O_RDONLY));
ASSERT_SYS(EACCES, -1, open("garden/secret.txt", O_RDONLY));
ASSERT_SYS(EACCES, -1, open("heaven/verysecret.txt", O_RDONLY));
EXITS(0);
}
TEST(unveil, truncate_isForbiddenBySeccomp) {
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, xbarf("garden/secret.txt", "hello", 5));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(IsOpenbsd() ? ENOENT : EPERM, -1,
truncate("garden/secret.txt", 0));
if (IsLinux()) {
ASSERT_SYS(0, 0, stat("garden/secret.txt", &st));
ASSERT_EQ(5, st.st_size);
}
EXITS(0);
}
TEST(unveil, ftruncate_isForbidden) {
if (IsOpenbsd()) return; // b/c O_PATH is a Linux thing
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, xbarf("garden/secret.txt", "hello", 5));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 3, open("garden/secret.txt", O_PATH));
ASSERT_SYS(EBADF, -1, ftruncate(3, 0));
ASSERT_SYS(0, 0, close(3));
ASSERT_SYS(0, 0, stat("garden/secret.txt", &st));
ASSERT_EQ(5, st.st_size);
EXITS(0);
}
TEST(unveil, procfs_isForbiddenByDefault) {
if (IsOpenbsd()) return;
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(EACCES, -1, open("/proc/self/cmdline", O_RDONLY));
EXITS(0);
}
int Worker(void *arg, int tid) {
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
return 0;
}
TEST(unveil, isInheritedAcrossThreads) {
struct spawn t;
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, xbarf("garden/secret.txt", "hello", 5));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 0, _spawn(Worker, 0, &t));
EXPECT_SYS(0, 0, _join(&t));
EXITS(0);
}
int Worker2(void *arg, int tid) {
ASSERT_SYS(0, 0, unveil("jail", "rw"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
return 0;
}
TEST(unveil, isThreadSpecificOnLinux_isProcessWideOnOpenbsd) {
struct spawn t;
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, xbarf("garden/secret.txt", "hello", 5));
ASSERT_SYS(0, 0, _spawn(Worker2, 0, &t));
EXPECT_SYS(0, 0, _join(&t));
if (IsOpenbsd()) {
ASSERT_SYS(ENOENT, -1, open("garden/secret.txt", O_RDONLY));
} else {
ASSERT_SYS(0, 3, open("garden/secret.txt", O_RDONLY));
}
EXITS(0);
}
TEST(unveil, usedTwice_forbidden_worksWithPledge) {
int ws, pid;
bool *gotsome;
ASSERT_NE(-1, (gotsome = mmap(0, FRAMESIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0)));
ASSERT_NE(-1, (pid = fork()));
if (!pid) {
// install our first seccomp filter
ASSERT_SYS(0, 0, pledge("stdio rpath wpath cpath unveil", 0));
ASSERT_SYS(0, 0, mkdir("jail", 0755));
ASSERT_SYS(0, 0, mkdir("garden", 0755));
ASSERT_SYS(0, 0, xbarf("garden/secret.txt", "hello", 5));
ASSERT_SYS(0, 0, unveil("jail", "rw"));
// committing and locking causes a new bpf filter to be installed
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
// verify the second filter is working
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
// verify the first filter is still working
*gotsome = true;
ASSERT_SYS(EPERM, -1, socket(AF_UNIX, SOCK_STREAM, 0));
if (IsLinux()) {
ASSERT_SYS(0, 0, stat("garden/secret.txt", &st));
ASSERT_EQ(5, st.st_size); // wut linux metadata is accessible
}
_Exit(0);
}
ASSERT_NE(-1, wait(&ws));
ASSERT_TRUE(*gotsome);
if (IsOpenbsd()) {
ASSERT_TRUE(WIFSIGNALED(ws));
ASSERT_EQ(SIGABRT, WTERMSIG(ws));
} else {
ASSERT_TRUE(WIFEXITED(ws));
ASSERT_EQ(0, WEXITSTATUS(ws));
}
EXPECT_SYS(0, 0, munmap(gotsome, FRAMESIZE));
}
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.
2022-07-20 04:18:33 +00:00
TEST(unveil, lotsOfPaths) {
int i, n;
SPAWN(fork);
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.
2022-07-20 04:18:33 +00:00
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);
}
TEST(unveil, reparent) {
return; // need abi 2 :'(
SPAWN(fork);
ASSERT_SYS(0, 0, mkdir("x", 0755));
ASSERT_SYS(0, 0, unveil("x", "rwc"));
ASSERT_SYS(0, 0, unveil(0, 0));
ASSERT_SYS(0, 0, mkdir("x/y", 0755));
ASSERT_SYS(0, 0, touch("x/y/z", 0644));
ASSERT_SYS(0, 0, rename("x/y/z", "x/z"));
EXITS(0);
}