mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-03-06 08:56:22 +00:00
Further improve unveil() implementation
This change addresses review comments from Günther Noack on GitHub. We're now blacklisting truncate() and setxattr() since Landlock lets them operate on veiled files. The restriction has been lifted on using unveil() multiple times, since Landlock does that well.
This commit is contained in:
parent
8593580d0a
commit
6c71bd5969
6 changed files with 154 additions and 43 deletions
|
@ -103,7 +103,7 @@ int geteuid(void) nosideeffect;
|
|||
int getgid(void) nosideeffect;
|
||||
int gethostname(char *, size_t);
|
||||
int getloadavg(double *, int);
|
||||
int getpgid(int) nosideeffect libcesque;
|
||||
int getpgid(int) libcesque;
|
||||
int getpgrp(void) nosideeffect;
|
||||
int getpid(void) nosideeffect libcesque;
|
||||
int getppid(void);
|
||||
|
@ -112,7 +112,7 @@ int getresgid(uint32_t *, uint32_t *, uint32_t *);
|
|||
int getresuid(uint32_t *, uint32_t *, uint32_t *);
|
||||
int getsid(int) nosideeffect libcesque;
|
||||
int gettid(void) libcesque;
|
||||
int getuid(void) nosideeffect libcesque;
|
||||
int getuid(void) libcesque;
|
||||
int ioprio_get(int, int);
|
||||
int ioprio_set(int, int, int);
|
||||
int kill(int, int);
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
#include "libc/calls/strace.internal.h"
|
||||
#include "libc/calls/syscall-sysv.internal.h"
|
||||
#include "libc/dce.h"
|
||||
#include "libc/runtime/runtime.h"
|
||||
#include "libc/sysv/consts/auxv.h"
|
||||
|
||||
/**
|
||||
* Returns effective group ID of calling process.
|
||||
|
@ -27,11 +29,13 @@
|
|||
*/
|
||||
int getegid(void) {
|
||||
int rc;
|
||||
if (!(rc = getauxval(AT_EGID))) {
|
||||
if (!IsWindows()) {
|
||||
rc = sys_getegid();
|
||||
} else {
|
||||
rc = getgid();
|
||||
}
|
||||
}
|
||||
STRACE("%s() → %d% m", "getegid", rc);
|
||||
return rc;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
#include "libc/calls/calls.h"
|
||||
#include "libc/calls/strace.internal.h"
|
||||
#include "libc/calls/syscall-sysv.internal.h"
|
||||
#include "libc/runtime/runtime.h"
|
||||
#include "libc/sysv/consts/auxv.h"
|
||||
|
||||
/**
|
||||
* Returns effective user ID of calling process.
|
||||
|
@ -26,11 +28,13 @@
|
|||
*/
|
||||
int geteuid(void) {
|
||||
int rc;
|
||||
if (!(rc = getauxval(AT_EUID))) {
|
||||
if (!IsWindows()) {
|
||||
rc = sys_geteuid();
|
||||
} else {
|
||||
rc = getuid();
|
||||
}
|
||||
}
|
||||
STRACE("%s() → %d% m", "geteuid", rc);
|
||||
return rc;
|
||||
}
|
||||
|
|
|
@ -53,11 +53,13 @@ static textwindows dontinline uint32_t GetUserNameHash(void) {
|
|||
*/
|
||||
int getuid(void) {
|
||||
int rc;
|
||||
if (!(rc = getauxval(AT_UID))) {
|
||||
if (!IsWindows()) {
|
||||
rc = sys_getuid();
|
||||
} else {
|
||||
rc = GetUserNameHash();
|
||||
}
|
||||
}
|
||||
STRACE("%s() → %d% m", "getuid", rc);
|
||||
return rc;
|
||||
}
|
||||
|
@ -73,11 +75,13 @@ int getuid(void) {
|
|||
*/
|
||||
int getgid(void) {
|
||||
int rc;
|
||||
if (!(rc = getauxval(AT_GID))) {
|
||||
if (!IsWindows()) {
|
||||
rc = sys_getgid();
|
||||
} else {
|
||||
rc = GetUserNameHash();
|
||||
}
|
||||
}
|
||||
STRACE("%s() → %d% m", "getgid", rc);
|
||||
return rc;
|
||||
}
|
||||
|
|
|
@ -64,18 +64,15 @@
|
|||
(LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE | \
|
||||
LANDLOCK_ACCESS_FS_EXECUTE)
|
||||
|
||||
static const struct sock_filter kBlacklistLandlock[] = {
|
||||
// clang-format off
|
||||
static const struct sock_filter kUnveilBlacklist[] = {
|
||||
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(arch)),
|
||||
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
|
||||
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
|
||||
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, OFF(nr)),
|
||||
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_linux_landlock_create_ruleset, 2, 0),
|
||||
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_linux_landlock_add_rule, 1, 0),
|
||||
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_linux_landlock_restrict_self, 0, 1),
|
||||
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_linux_truncate, 1, 0),
|
||||
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_linux_setxattr, 0, 1),
|
||||
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (1 & SECCOMP_RET_DATA)),
|
||||
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -98,8 +95,8 @@ _Thread_local static struct {
|
|||
static int unveil_final(void) {
|
||||
int rc;
|
||||
struct sock_fprog sandbox = {
|
||||
.filter = kBlacklistLandlock,
|
||||
.len = ARRAYLEN(kBlacklistLandlock),
|
||||
.filter = kUnveilBlacklist,
|
||||
.len = ARRAYLEN(kUnveilBlacklist),
|
||||
};
|
||||
if ((rc = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) != -1 &&
|
||||
(rc = landlock_restrict_self(State.fd, 0)) != -1 &&
|
||||
|
@ -264,14 +261,13 @@ static int sys_unveil_linux(const char *path, const char *permissions) {
|
|||
* unveil("/etc", "r"); // make /etc readable too
|
||||
* unveil(0, 0); // commit and lock policy
|
||||
*
|
||||
* Unveiling restricts a thread's view of the filesystem to a set of
|
||||
* allowed paths with specific privileges.
|
||||
* Unveiling restricts a view of the filesystem to a set of allowed
|
||||
* paths with specific privileges.
|
||||
*
|
||||
* Once you start using unveil(), the entire file system is considered
|
||||
* hidden. You then specify, by repeatedly calling unveil(), which paths
|
||||
* should become unhidden. When you're finished, you call `unveil(0,0)`
|
||||
* which commits your policy, after which further use is forbidden, in
|
||||
* the current thread, as well as any threads or processes it spawns.
|
||||
* which commits your policy.
|
||||
*
|
||||
* There are some differences between unveil() on Linux versus OpenBSD.
|
||||
*
|
||||
|
@ -299,8 +295,28 @@ static int sys_unveil_linux(const char *path, const char *permissions) {
|
|||
* possible to use opendir() and go fishing for paths which weren't
|
||||
* previously known.
|
||||
*
|
||||
* 5. Always specify at least one path. OpenBSD has unclear semantics
|
||||
* when `pledge(0,0)` is used without any previous calls.
|
||||
* 5. Use ftruncate() rather than truncate(). One of the backdoors with
|
||||
* Landlock is it currently can't restrict truncate() and setxattr()
|
||||
* which permits certain kinds of modifications to files outside the
|
||||
* sandbox. When your policy is committed, we install a SECCOMP BPF
|
||||
* filter to disable those calls, however similar trickery may be
|
||||
* possible through other unaddressed calls like ioctl(). Using the
|
||||
* pledge() function in addition to unveil() will solve this, since
|
||||
* it installs a strong system call access policy.
|
||||
*
|
||||
* 6. Set your process-wide policy at startup from the main thread. On
|
||||
* OpenBSD unveil() will apply process-wide even when called from a
|
||||
* child thread; whereas with Linux, calling unveil() from a thread
|
||||
* will cause your ruleset to only apply to that thread in addition
|
||||
* to any descendent threads it creates.
|
||||
*
|
||||
* 7. Always specify at least one path. OpenBSD has unclear semantics
|
||||
* when `unveil(0,0)` is used without any previous calls.
|
||||
*
|
||||
* 8. On OpenBSD calling `unveil(0,0)` will prevent unveil() from being
|
||||
* used again. On Linux this is allowed, because Landlock is able to
|
||||
* do that securely, i.e. the second ruleset can only be a subset of
|
||||
* the previous ones.
|
||||
*
|
||||
* This system call is supported natively on OpenBSD and polyfilled on
|
||||
* Linux using the Landlock LSM[1].
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
#include "libc/sysv/consts/sig.h"
|
||||
#include "libc/sysv/consts/sock.h"
|
||||
#include "libc/testlib/testlib.h"
|
||||
#include "libc/thread/spawn.h"
|
||||
#include "libc/x/x.h"
|
||||
|
||||
STATIC_YOINK("zip_uri_support");
|
||||
|
@ -241,19 +242,107 @@ TEST(unveil, overlappingDirectories_inconsistentBehavior) {
|
|||
EXITS(0);
|
||||
}
|
||||
|
||||
TEST(unveil, usedTwice_forbidden) {
|
||||
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(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
|
||||
ASSERT_SYS(EPERM, -1, unveil("jail", "rw"));
|
||||
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
|
||||
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); // wut linux metadata is accessible
|
||||
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);
|
||||
}
|
||||
|
@ -275,12 +364,6 @@ TEST(unveil, usedTwice_forbidden_worksWithPledge) {
|
|||
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(EPERM, -1, unveil("jail", "rw"));
|
||||
if (IsLinux()) {
|
||||
ASSERT_SYS(
|
||||
EPERM, -1,
|
||||
landlock_create_ruleset(0, 0, LANDLOCK_CREATE_RULESET_VERSION));
|
||||
}
|
||||
ASSERT_SYS(EACCES_OR_ENOENT, -1, open("garden/secret.txt", O_RDONLY));
|
||||
// verify the first filter is still working
|
||||
*gotsome = true;
|
||||
|
|
Loading…
Add table
Reference in a new issue