Fix bugs and regressions in the pledge command

This change gets the pledge (formerly pledge.com) command back in tip
top shape for a 3.0.1 cosmos release. It now runs on all platforms, even
though it's mostly a no-op on ones that lack the kernel security stuff.
The binary footprint is now smaller, since it no longer needs to link
malloc. It's also now able to be built as a fat binary.
This commit is contained in:
Justine Tunney 2023-11-01 06:08:58 -07:00
parent b0e3d89942
commit 7b284f6bda
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
18 changed files with 493 additions and 272 deletions

View file

@ -77,7 +77,7 @@ o/$(MODE)/tool/build/%.com.dbg: \
$(APE_NO_MODIFY_SELF)
@$(APELINK)
o/$(MODE)/tool/build/dso/sandbox.so.zip.o \
o/$(MODE)/tool/build/dso/sandbox-$(ARCH).so.zip.o \
o/$(MODE)/tool/build/false.com.zip.o \
o/$(MODE)/tool/build/echo.com.zip.o \
o/$(MODE)/tool/build/cocmd.com.zip.o: private \
@ -99,7 +99,7 @@ o/$(MODE)/tool/build/dso/sandbox.o: \
libc/intrin/promises.internal.h \
tool/build/build.mk
o/$(MODE)/tool/build/dso/sandbox.so: \
o/$(MODE)/tool/build/dso/sandbox-$(ARCH).so: \
o/$(MODE)/tool/build/dso/sandbox.o \
o/$(MODE)/libc/calls/pledge-linux.o \
o/$(MODE)/libc/sysv/restorert.o
@ -118,7 +118,7 @@ o/$(MODE)/tool/build/dso/sandbox.so: \
o/$(MODE)/tool/build/pledge.com.dbg: \
$(TOOL_BUILD_DEPS) \
o/$(MODE)/tool/build/build.pkg \
o/$(MODE)/tool/build/dso/sandbox.so.zip.o \
o/$(MODE)/tool/build/dso/sandbox-$(ARCH).so.zip.o \
o/$(MODE)/tool/build/pledge.o \
$(CRT) \
$(APE_NO_MODIFY_SELF)

View file

@ -22,10 +22,8 @@
#include "libc/intrin/promises.internal.h"
#include "libc/runtime/runtime.h"
/*
* runs pledge at glibc executable load time, e.g.
* strace -vff bash -c '_PLEDGE=4194303,0 LD_PRELOAD=$HOME/sandbox.so ls'
*/
// runs pledge at glibc executable load time, e.g.
// strace -vff bash -c '_PLEDGE=4194303,0 LD_PRELOAD=$HOME/sandbox.so ls'
__attribute__((__constructor__)) void init(void) {
int c, i, j;

View file

@ -17,7 +17,7 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/pledge.h"
#include "libc/assert.h"
#include "ape/ape.h"
#include "libc/calls/calls.h"
#include "libc/calls/landlock.h"
#include "libc/calls/pledge.internal.h"
@ -26,6 +26,7 @@
#include "libc/calls/struct/seccomp.internal.h"
#include "libc/calls/struct/stat.h"
#include "libc/calls/struct/sysinfo.h"
#include "libc/calls/syscall-nt.internal.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/calls/syscall_support-sysv.internal.h"
#include "libc/dce.h"
@ -35,6 +36,7 @@
#include "libc/elf/struct/phdr.h"
#include "libc/errno.h"
#include "libc/fmt/conv.h"
#include "libc/fmt/itoa.h"
#include "libc/intrin/bits.h"
#include "libc/intrin/kprintf.h"
#include "libc/intrin/promises.internal.h"
@ -42,13 +44,14 @@
#include "libc/limits.h"
#include "libc/macros.internal.h"
#include "libc/math.h"
#include "libc/mem/gc.internal.h"
#include "libc/mem/mem.h"
#include "libc/mem/alloca.h"
#include "libc/nexgen32e/kcpuids.h"
#include "libc/runtime/runtime.h"
#include "libc/runtime/stack.h"
#include "libc/sock/sock.h"
#include "libc/sock/struct/pollfd.h"
#include "libc/stdio/stdio.h"
#include "libc/stdio/sysparam.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/ioprio.h"
#include "libc/sysv/consts/map.h"
@ -75,7 +78,7 @@ __static_yoink("zipos");
#define USAGE \
"\
usage: pledge.com [-hnN] PROG ARGS...\n\
usage: pledge [-hnN] PROG ARGS...\n\
-h show help\n\
-g GID call setgid()\n\
-u UID call setuid()\n\
@ -116,7 +119,7 @@ usage: pledge.com [-hnN] PROG ARGS...\n\
- vminfo: allows /proc/stat, /proc/self/maps, etc.\n\
- tmppath: allows /tmp, $TMPPATH, lstat, unlink\n\
\n\
pledge.com v1.8\n\
Cosompolitan Pledge v1.9\n\
copyright 2022 justine alexandra roberts tunney\n\
notice licenses are embedded in the binary\n\
https://twitter.com/justinetunney\n\
@ -130,13 +133,27 @@ the https://justine.lol/pledge/ page for online documentation.\n\
\n\
"
#ifdef __x86_64__
#define ARCH_NAME "x86_64"
#elif defined(__aarch64__)
#define ARCH_NAME "aarch64"
#else
#error "unsupported architecture"
#endif
enum Strategy {
kStrategyNull,
kStrategyStatic,
kStrategyDynamic,
kStrategyApe,
};
int g_gflag;
int g_uflag;
int g_kflag;
int g_hflag;
bool g_nice;
bool g_qflag;
bool isdynamic;
bool g_noclose;
long g_cpuquota;
long g_fszquota;
@ -147,19 +164,60 @@ long g_dontdrop;
long g_dontunveil;
const char *g_test;
const char *g_chroot;
const char *g_promises;
char pledgevar[64];
char g_promises[256];
char dsopath[PATH_MAX];
char tmppath[PATH_MAX];
char preloadvar[PATH_MAX];
struct {
int n;
char **p;
char *p[10000];
} unveils;
static void GetOpts(int argc, char *argv[]) {
unsigned long HasPromise(unsigned long ipromises, int promise) {
return ~ipromises & (1ul << promise);
}
long Atoi(const char *s) {
long i;
char *ep;
errno = 0;
i = strtol(s, &ep, 0);
if (*ep || errno) {
tinyprint(2, program_invocation_name, ": invalid integer: ", s, "\n", NULL);
exit(1);
}
return i;
}
long ParseSiSize(const char *s, long b) {
long i;
errno = 0;
i = sizetol(s, b);
if (errno) {
tinyprint(2, program_invocation_name, ": invalid size: ", s, "\n", NULL);
exit(1);
}
return i;
}
void AddPromise(const char *s) {
while (isspace(*s)) ++s;
if (!*s) return;
if (*g_promises) {
strlcat(g_promises, " ", sizeof(g_promises));
}
if (strlcat(g_promises, s, sizeof(g_promises)) >= sizeof(g_promises)) {
tinyprint(2, program_invocation_name, ": too many promises\n", NULL);
exit(1);
}
}
void GetOpts(int argc, char *argv[]) {
int opt;
struct sysinfo si;
g_promises = 0;
bool got_promise_flag = false;
g_nfdquota = 64;
g_fszquota = 256 * 1000 * 1000;
if (!sysinfo(&si)) {
@ -196,46 +254,36 @@ static void GetOpts(int argc, char *argv[]) {
g_chroot = optarg;
break;
case 'g':
g_gflag = atoi(optarg);
g_gflag = Atoi(optarg);
break;
case 'u':
g_uflag = atoi(optarg);
g_uflag = Atoi(optarg);
break;
case 'C':
g_cpuquota = atoi(optarg);
g_cpuquota = Atoi(optarg);
break;
case 'P':
g_proquota = atoi(optarg);
g_proquota = Atoi(optarg);
break;
case 'O':
g_nfdquota = atoi(optarg);
g_nfdquota = Atoi(optarg);
break;
case 'F':
errno = 0;
g_fszquota = sizetol(optarg, 1000);
if (errno) {
kprintf("error: invalid size: -F %s\n", optarg);
exit(1);
}
g_fszquota = ParseSiSize(optarg, 1024);
break;
case 'M':
errno = 0;
g_memquota = sizetol(optarg, 1024);
if (errno) {
kprintf("error: invalid size: -F %s\n", optarg);
exit(1);
}
g_memquota = ParseSiSize(optarg, 1024);
break;
case 'p':
if (g_promises) {
g_promises = xstrcat(g_promises, ' ', optarg);
} else {
g_promises = optarg;
}
AddPromise(optarg);
got_promise_flag = true;
break;
case 'v':
unveils.p = realloc(unveils.p, ++unveils.n * sizeof(*unveils.p));
unveils.p[unveils.n - 1] = optarg;
if (unveils.n == ARRAYLEN(unveils.p)) {
tinyprint(2, program_invocation_name, ": too many unveils\n", NULL);
exit(1);
}
unveils.p[unveils.n++] = optarg;
break;
case 'h':
case '?':
@ -246,16 +294,16 @@ static void GetOpts(int argc, char *argv[]) {
exit(64);
}
}
if (!g_promises) {
g_promises = "stdio rpath";
if (!got_promise_flag) {
stpcpy(g_promises, "stdio rpath");
}
}
const char *prog;
const char *g_prog;
char pathbuf[PATH_MAX];
struct pollfd pfds[256];
static bool SupportsLandlock(void) {
bool SupportsLandlock(void) {
int e = errno;
bool r = landlock_create_ruleset(0, 0, LANDLOCK_CREATE_RULESET_VERSION) >= 0;
errno = e;
@ -265,7 +313,7 @@ static bool SupportsLandlock(void) {
int GetPollMaxFds(void) {
int n;
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) != -1) {
if (!getrlimit(RLIMIT_NOFILE, &rl)) {
n = rl.rlim_cur;
} else {
n = 64;
@ -274,27 +322,26 @@ int GetPollMaxFds(void) {
}
void NormalizeFileDescriptors(void) {
int e, i, n, fd;
int i, n, fd;
closefrom(3); // faster and more secure if linux 5.9+ or bsd
n = GetPollMaxFds();
e = errno;
closefrom(3); // more secure if linux 5.9+
errno = e;
for (i = 0; i < n; ++i) {
pfds[i].fd = i;
pfds[i].events = POLLIN;
}
if (poll(pfds, n, 0) == -1) {
kprintf("error: poll() failed: %m\n");
perror("poll");
exit(1);
}
for (i = 0; i < 3; ++i) {
if (pfds[i].revents & POLLNVAL) {
if ((fd = open("/dev/null", O_RDWR)) == -1) {
kprintf("error: open(\"/dev/null\") failed: %m\n");
perror("/dev/null");
exit(2);
}
if (fd != i) {
kprintf("error: open() is broken: %d vs. %d\n", fd, i);
tinyprint(2, program_invocation_name, ": poll() or open() is broken\n",
NULL);
exit(3);
}
}
@ -302,7 +349,7 @@ void NormalizeFileDescriptors(void) {
for (i = 3; i < n; ++i) {
if (~pfds[i].revents & POLLNVAL) {
if (close(pfds[i].fd) == -1) {
kprintf("error: close(%d) failed: %m\n", pfds[i].fd);
perror("close");
exit(4);
}
}
@ -354,8 +401,8 @@ bool PathExists(const char *path) {
void Unveil(const char *path, const char *perm) {
if (unveil(path, perm) == -1) {
kprintf("error: unveil(%#s, %#s) failed: %m\n", path, perm);
_Exit(20);
perror(path);
exit(20);
}
}
@ -368,22 +415,62 @@ int UnveilIfExists(const char *path, const char *perm) {
} else if (errno == ENOENT) {
errno = err;
} else {
kprintf("error: unveil(%#s, %#s) failed: %m\n", path, perm);
_Exit(20);
perror(path);
exit(20);
}
}
return -1;
}
enum Strategy GetStrategy(void) {
static enum Strategy strategy;
if (strategy == kStrategyNull) {
strategy = kStrategyStatic;
int fd;
if ((fd = open(g_prog, O_RDONLY)) != -1) {
union {
char magic[8];
Elf64_Ehdr ehdr;
} hdr = {0};
if (pread(fd, &hdr, sizeof(hdr), 0) == sizeof(hdr)) {
if (READ64LE(hdr.magic) == READ64LE("MZqFpD='") ||
READ64LE(hdr.magic) == READ64LE("jartsr='") ||
READ64LE(hdr.magic) == READ64LE("APEDBG='")) {
strategy = kStrategyApe;
} else if ((IsLinux() || IsFreebsd() || IsNetbsd() || IsOpenbsd()) &&
IsElf64Binary(&hdr.ehdr, sizeof(hdr))) {
if (hdr.ehdr.e_type == ET_DYN) {
strategy = kStrategyDynamic;
} else {
Elf64_Phdr phdrs[16];
int count = MIN(hdr.ehdr.e_phnum, ARRAYLEN(phdrs));
int bytes = count * sizeof(Elf64_Phdr);
if (pread(fd, phdrs, bytes, hdr.ehdr.e_phoff) == bytes) {
for (int i = 0; i < count; ++i) {
if (phdrs[i].p_type == PT_INTERP ||
phdrs[i].p_type == PT_DYNAMIC) {
strategy = kStrategyDynamic;
}
}
}
}
}
}
close(fd);
}
}
return strategy;
}
void ApplyFilesystemPolicy(unsigned long ipromises) {
const char *p;
if (g_dontunveil) return;
if (!SupportsLandlock()) return;
Unveil(prog, "rx");
Unveil(g_prog, "rx");
if (isdynamic) {
if (GetStrategy() == kStrategyDynamic) {
Unveil(dsopath, "rx");
UnveilIfExists("/lib", "rx");
UnveilIfExists("/lib64", "rx");
@ -391,14 +478,16 @@ void ApplyFilesystemPolicy(unsigned long ipromises) {
UnveilIfExists("/usr/lib64", "rx");
UnveilIfExists("/usr/local/lib", "rx");
UnveilIfExists("/usr/local/lib64", "rx");
UnveilIfExists("/etc/ld-musl-x86_64.path", "r");
UnveilIfExists("/etc/ld-musl-" ARCH_NAME ".path", "r");
UnveilIfExists("/etc/ld.so.conf", "r");
UnveilIfExists("/etc/ld.so.cache", "r");
UnveilIfExists("/etc/ld.so.conf.d", "r");
UnveilIfExists("/etc/ld.so.preload", "r");
// in case musl is symlinked somewhere else
UnveilIfExists("/lib/ld-musl-" ARCH_NAME ".so.1", "rx");
}
if (~ipromises & (1ul << PROMISE_STDIO)) {
if (HasPromise(ipromises, PROMISE_STDIO)) {
UnveilIfExists("/dev/fd", "r");
UnveilIfExists("/dev/log", "w");
UnveilIfExists("/dev/zero", "r");
@ -422,15 +511,15 @@ void ApplyFilesystemPolicy(unsigned long ipromises) {
UnveilIfExists("/proc/sys/vm/overcommit_memory", "r");
}
if (~ipromises & (1ul << PROMISE_INET)) {
if (HasPromise(ipromises, PROMISE_INET)) {
UnveilIfExists("/etc/ssl/certs/ca-certificates.crt", "r");
}
if (~ipromises & (1ul << PROMISE_RPATH)) {
if (HasPromise(ipromises, PROMISE_RPATH)) {
UnveilIfExists("/proc/filesystems", "r");
}
if (~ipromises & (1ul << PROMISE_DNS)) {
if (HasPromise(ipromises, PROMISE_DNS)) {
UnveilIfExists("/etc/hosts", "r");
UnveilIfExists("/etc/hostname", "r");
UnveilIfExists("/etc/services", "r");
@ -438,7 +527,7 @@ void ApplyFilesystemPolicy(unsigned long ipromises) {
UnveilIfExists("/etc/resolv.conf", "r");
}
if (~ipromises & (1ul << PROMISE_TTY)) {
if (HasPromise(ipromises, PROMISE_TTY)) {
UnveilIfExists(ttyname(0), "rw");
UnveilIfExists("/dev/tty", "rw");
UnveilIfExists("/dev/console", "rw");
@ -447,18 +536,21 @@ void ApplyFilesystemPolicy(unsigned long ipromises) {
UnveilIfExists("/usr/share/terminfo", "r");
}
if (~ipromises & (1ul << PROMISE_PROT_EXEC)) {
if (GetStrategy() == kStrategyApe) {
if (UnveilIfExists("/usr/bin/ape", "rx") == -1) {
char buf[PATH_MAX];
if ((p = getenv("TMPDIR"))) {
UnveilIfExists(xjoinpaths(p, ".ape"), "rx");
UnveilIfExists(
__join_paths(buf, sizeof(buf), p, ".ape-" APE_VERSION_STR), "rx");
}
if ((p = getenv("HOME"))) {
UnveilIfExists(xjoinpaths(p, ".ape"), "rx");
UnveilIfExists(
__join_paths(buf, sizeof(buf), p, ".ape-" APE_VERSION_STR), "rx");
}
}
}
if (~ipromises & (1ul << PROMISE_VMINFO)) {
if (HasPromise(ipromises, PROMISE_VMINFO)) {
UnveilIfExists("/proc/stat", "r");
UnveilIfExists("/proc/meminfo", "r");
UnveilIfExists("/proc/cpuinfo", "r");
@ -467,7 +559,7 @@ void ApplyFilesystemPolicy(unsigned long ipromises) {
UnveilIfExists("/sys/devices/system/cpu", "r");
}
if (~ipromises & (1ul << PROMISE_TMPPATH)) {
if (HasPromise(ipromises, PROMISE_TMPPATH)) {
UnveilIfExists("/tmp", "rwc");
UnveilIfExists(getenv("TMPPATH"), "rwc");
}
@ -489,21 +581,22 @@ void ApplyFilesystemPolicy(unsigned long ipromises) {
}
if (unveil(0, 0) == -1) {
kprintf("error: unveil(0, 0) failed: %m\n");
_Exit(20);
perror("unveil");
exit(20);
}
}
void DropCapabilities(void) {
int e, i;
if (!IsLinux()) return;
for (e = errno, i = 0;; ++i) {
if (prctl(PR_CAPBSET_DROP, i) == -1) {
if (errno == EINVAL || errno == EPERM) {
errno = e;
break;
} else {
kprintf("error: prctl(PR_CAPBSET_DROP, %d) failed: %m\n", i);
_Exit(25);
perror("prctl(PR_CAPBSET_DROP)");
exit(25);
}
}
}
@ -533,68 +626,20 @@ int Extract(const char *from, const char *to, int mode) {
return close(fdout) | close(fdin);
}
/**
* Returns true if ELF executable uses dynamic loading magic.
*/
static bool IsDynamicExecutable(const char *prog) {
bool res;
Elf64_Ehdr *e;
Elf64_Phdr *p;
struct stat st;
int i, fd, err;
fd = -1;
err = errno;
e = MAP_FAILED;
if ((fd = open(prog, O_RDONLY)) == -1) {
res = false;
goto Finish;
}
if (fstat(fd, &st) == -1 || st.st_size < 64) {
res = false;
goto Finish;
}
if ((e = mmap(0, st.st_size, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
res = false;
goto Finish;
}
if (READ32LE(e->e_ident) != READ32LE(ELFMAG)) {
res = false;
goto Finish;
}
if (e->e_type == ET_DYN) {
res = true;
goto Finish;
}
for (i = 0; i < e->e_phnum; ++i) {
p = GetElfProgramHeaderAddress(e, st.st_size, i);
if (p->p_type == PT_INTERP || p->p_type == PT_DYNAMIC) {
res = true;
goto Finish;
}
}
res = false;
goto Finish;
Finish:
if (e != MAP_FAILED) munmap(e, st.st_size);
if (fd != -1) close(fd);
errno = err;
int CountEnviron(char **ep) {
int res = 0;
while (*ep++) ++res;
return res;
}
int main(int argc, char *argv[]) {
const char *s;
bool hasfunbits;
char buf[PATH_MAX];
int useruid, usergid;
int owneruid, ownergid;
int oldfsuid, oldfsgid;
unsigned long ipromises;
if (!IsLinux()) {
kprintf("error: this program is only intended for linux\n");
exit(5);
}
// parse flags
GetOpts(argc, argv);
if (g_test) {
@ -612,41 +657,33 @@ int main(int argc, char *argv[]) {
exit(1);
}
}
kprintf("error: unknown test: %s\n", g_test);
tinyprint(2, g_test, ": unknown test\n", NULL);
exit(2);
}
if (optind == argc) {
kprintf("error: too few args\n");
tinyprint(2, "error: missing command\n", NULL);
write(2, USAGE, sizeof(USAGE) - 1);
exit(64);
}
// perform process setup
if (!g_noclose) {
NormalizeFileDescriptors();
}
if (g_nice) {
verynice();
}
if (SetCpuLimit(g_cpuquota) == -1) {
kprintf("error: setrlimit(%s) failed: %m\n", "RLIMIT_CPU");
exit(1);
perror("setrlimit(RLIMIT_CPU)");
}
if (SetLimit(RLIMIT_FSIZE, g_fszquota, g_fszquota * 1.5) == -1) {
kprintf("error: setrlimit(%s) failed: %m\n", "RLIMIT_FSIZE");
exit(1);
perror("setrlimit(RLIMIT_FSIZE)");
}
if (SetLimit(RLIMIT_AS, g_memquota, g_memquota) == -1) {
kprintf("error: setrlimit(%s) failed: %m\n", "RLIMIT_AS");
exit(1);
perror("setrlimit(RLIMIT_AS)");
}
if (SetLimit(RLIMIT_NPROC, g_proquota, g_proquota) == -1) {
kprintf("error: setrlimit(%s) failed: %m\n", "RLIMIT_NPROC");
exit(1);
perror("setrlimit(RLIMIT_NPROC)");
}
// test for weird chmod bits
@ -664,8 +701,9 @@ int main(int argc, char *argv[]) {
// some flags can't be allowed if binary has setuid bits
if (hasfunbits) {
if (g_uflag || g_gflag) {
kprintf("error: setuid flags forbidden on setuid binaries\n");
_Exit(6);
tinyprint(2, program_invocation_name,
": setuid flags forbidden on setuid binaries\n", NULL);
exit(6);
}
}
@ -674,8 +712,8 @@ int main(int argc, char *argv[]) {
oldfsuid = setfsuid(useruid);
oldfsgid = setfsgid(usergid);
if (access(g_chroot, R_OK) == -1) {
kprintf("error: access(%#s) failed: %m\n", g_chroot);
_Exit(7);
perror(g_chroot);
exit(7);
}
setfsuid(oldfsuid);
setfsgid(oldfsgid);
@ -684,12 +722,12 @@ int main(int argc, char *argv[]) {
// change root fs path
if (g_chroot) {
if (chdir(g_chroot) == -1) {
kprintf("error: chdir(%#s) failed: %m\n", g_chroot);
_Exit(8);
perror(g_chroot);
exit(8);
}
if (chroot(g_chroot) == -1) {
kprintf("error: chroot(%#s) failed: %m\n", g_chroot);
_Exit(9);
perror(g_chroot);
exit(9);
}
}
@ -698,42 +736,63 @@ int main(int argc, char *argv[]) {
oldfsuid = setfsuid(useruid);
oldfsgid = setfsgid(usergid);
}
if (!(prog = commandv(argv[optind], pathbuf, sizeof(pathbuf)))) {
kprintf("error: command not found: %m\n", argv[optind]);
_Exit(10);
if (!(g_prog = commandv(argv[optind], pathbuf, sizeof(pathbuf)))) {
perror(argv[optind]);
exit(10);
}
if (hasfunbits) {
setfsuid(oldfsuid);
setfsgid(oldfsgid);
}
// copy environment
// to setup child environment
// and remove variables we might create
unsetenv("_PLEDGE");
unsetenv("LD_PRELOAD");
int child_environ_count = CountEnviron(environ);
int child_environ_capacity = child_environ_count + 2 + 1;
int child_environ_bytes = child_environ_capacity * sizeof(char *);
char **child_environ = alloca(child_environ_bytes);
CheckLargeStackAllocation(child_environ, child_environ_bytes);
memcpy(child_environ, environ, child_environ_count * sizeof(char *));
bzero(child_environ + child_environ_count,
(child_environ_capacity - child_environ_count) * sizeof(char *));
// figure out where we want the dso
if (IsDynamicExecutable(prog)) {
isdynamic = true;
if (GetStrategy() == kStrategyDynamic) {
if ((s = getenv("TMPDIR")) || //
(s = getenv("HOME")) || //
(s = ".")) {
ksnprintf(dsopath, sizeof(dsopath), "%s/sandbox.so", s);
strlcpy(dsopath, s, sizeof(dsopath));
strlcat(dsopath, "/.pledge-sandbox.so", sizeof(dsopath));
if (!FileExistsAndIsNewerThan(dsopath, GetProgramExecutableName())) {
ksnprintf(tmppath, sizeof(tmppath), "%s/sandbox.so.%d", s, getpid());
if (Extract("/zip/sandbox.so", tmppath, 0755) == -1) {
kprintf("error: extract dso failed: %m\n");
errno = 0;
char pidstr[21];
FormatInt64(pidstr, getpid());
strlcpy(tmppath, s, sizeof(tmppath));
strlcat(tmppath, "/.pledge-sandbox.so.", sizeof(tmppath));
strlcat(tmppath, pidstr, sizeof(tmppath));
if (Extract("/zip/sandbox-" ARCH_NAME ".so", tmppath, 0755) == -1) {
perror(tmppath);
exit(1);
}
if (rename(tmppath, dsopath) == -1) {
kprintf("error: rename dso failed: %m\n");
perror(dsopath);
exit(1);
}
}
ksnprintf(buf, sizeof(buf), "LD_PRELOAD=%s", dsopath);
putenv(buf);
stpcpy(preloadvar, "LD_PRELOAD=");
strlcat(preloadvar, dsopath, sizeof(preloadvar));
child_environ[child_environ_count++] = preloadvar;
}
}
if (g_dontdrop) {
if (hasfunbits) {
kprintf("error: -D flag forbidden on setuid binaries\n");
_Exit(6);
tinyprint(2, program_invocation_name,
": -D flag forbidden on setuid binaries\n", NULL);
exit(6);
}
} else {
DropCapabilities();
@ -743,22 +802,22 @@ int main(int argc, char *argv[]) {
if (usergid != ownergid) {
// setgid binaries must use the gid of the user that ran it
if (setgid(usergid) == -1) {
kprintf("error: setgid(%d) failed: %m\n", usergid);
_Exit(11);
perror("setgid");
exit(11);
}
if (getgid() != usergid || getegid() != usergid) {
kprintf("error: setgid() broken\n");
_Exit(12);
tinyprint(2, "error: setgid() broken\n", NULL);
exit(12);
}
} else if (g_gflag) {
// otherwise we trust the gid flag
if (setgid(g_gflag) == -1) {
kprintf("error: setgid(%d) failed: %m\n", g_gflag);
_Exit(13);
perror("setgid");
exit(13);
}
if (getgid() != g_gflag || getegid() != g_gflag) {
kprintf("error: setgid() broken\n");
_Exit(14);
tinyprint(2, "error: setgid() broken\n", NULL);
exit(14);
}
}
@ -766,30 +825,34 @@ int main(int argc, char *argv[]) {
if (useruid != owneruid) {
// setuid binaries must use the uid of the user that ran it
if (setuid(useruid) == -1) {
kprintf("error: setuid(%d) failed: %m\n", useruid);
_Exit(15);
perror("setuid");
exit(15);
}
if (getuid() != useruid || geteuid() != useruid) {
kprintf("error: setuid() broken\n");
_Exit(16);
tinyprint(2, "error: setuid() broken\n", NULL);
exit(16);
}
} else if (g_uflag) {
// otherwise we trust the uid flag
if (setuid(g_uflag) == -1) {
kprintf("error: setuid(%d) failed: %m\n", g_uflag);
_Exit(17);
perror("setuid");
exit(17);
}
if (getuid() != g_uflag || geteuid() != g_uflag) {
kprintf("error: setuid() broken\n");
_Exit(18);
tinyprint(2, "error: setuid() broken\n", NULL);
exit(18);
}
}
// parse requested promises
// further changes to g_promises will be *effective* transient promises
if (ParsePromises(g_promises, &ipromises, 0) == -1) {
kprintf("error: bad promises list: %s\n", g_promises);
_Exit(21);
tinyprint(2, program_invocation_name, ": bad promises list: ", g_promises,
"\n", NULL);
exit(21);
}
// perform unveiling
ApplyFilesystemPolicy(ipromises);
// pledge.com uses the return eperm instead of killing the process
@ -802,38 +865,70 @@ int main(int argc, char *argv[]) {
__pledge_mode = PLEDGE_PENALTY_RETURN_EPERM;
}
// we need to be able to call execv and mmap the dso
// it'll be pledged away once/if the dso gets loaded
if (!(~ipromises & (1ul << PROMISE_EXEC))) {
g_promises = xstrcat(g_promises, ' ', "exec");
// weaken system call policy to allow execution
//
// 1. we always need to pledge("exec") in order to use execve(). this
// is the primary disadvantage to using the `pledge` command to
// bolt security onto unsecured programs (as opposed to using
// pledge as it was intended, by having the program authors update
// their code to invoke the pledge() system call from within their
// programs at the appropriate moments)
//
// 2. we usually need to force pledge("rpath prot_exec") too; dynamic
// executables need it mmap() shared objects during initialization
// and actually portable executables need it so /usr/bin/ape can
// mmap() the ELF program headers. the only time we don't require
// `prot_exec` is when launching a native ELF PT_EXEC binaries,
// e.g. assimilated actually portable executables.
//
// 3. in some cases we can remove the `exec` and `prot_exec` promises
// later on in the loading process. on musl and glibc systems, we
// do that by injecting an LD_PRELOAD DSO which calls pledge()
// again with the requested promises.
if (!HasPromise(ipromises, PROMISE_EXEC)) {
AddPromise("exec");
if (!g_qflag) {
// TODO(jart): Fix me.
// __pledge_mode |= PLEDGE_STDERR_LOGGING;
}
}
if (isdynamic) {
g_promises = xstrcat(g_promises, ' ', "prot_exec");
if (GetStrategy() != kStrategyStatic) {
if (!HasPromise(ipromises, PROMISE_RPATH)) {
AddPromise("rpath");
}
if (!HasPromise(ipromises, PROMISE_PROT_EXEC)) {
AddPromise("prot_exec");
}
}
// pass arguments to pledge() inside the dso
if (isdynamic) {
ksnprintf(buf, sizeof(buf), "_PLEDGE=%ld,%ld", ~ipromises, __pledge_mode);
putenv(buf);
// pass parameters to injected dso
if (GetStrategy() == kStrategyDynamic) {
char *p = pledgevar;
p = stpcpy(p, "_PLEDGE=");
p = FormatInt64(p, ~ipromises);
p = stpcpy(p, ",");
p = FormatInt64(p, __pledge_mode);
child_environ[child_environ_count++] = pledgevar;
}
// this limit needs to come last since unveil() opens fds
if (SetLimit(RLIMIT_NOFILE, g_nfdquota, g_nfdquota) == -1) {
kprintf("error: setrlimit(%s) failed: %m\n", "RLIMIT_NOFILE");
perror("setrlimit(RLIMIT_NOFILE)");
exit(1);
}
// apply sandbox
if (pledge(g_promises, g_promises) == -1) {
kprintf("error: pledge(%#s) failed: %m\n", g_promises);
_Exit(19);
perror("pledge");
exit(19);
}
// launch program
sys_execve(prog, argv + optind, environ);
kprintf("%s: execve failed: %m\n", prog);
if (!IsWindows()) {
sys_execve(g_prog, argv + optind, child_environ);
} else {
sys_execve_nt(g_prog, argv + optind, child_environ);
}
perror(g_prog);
return 127;
}

61
tool/build/verynice.c Normal file
View file

@ -0,0 +1,61 @@
/*-*- 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 2023 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/runtime/runtime.h"
#include "libc/stdio/stdio.h"
#include "third_party/getopt/getopt.internal.h"
/**
* @fileoverview `verynice foo` launches `foo` as low priority as possible
*
* This is particularly useful on Linux systems with a spinning disk
* where the classic `nice` command doesn't do much.
*/
const char *prog;
static wontreturn void PrintUsage(int rc, int fd) {
tinyprint(fd, "Usage: ", prog, " COMMAND...\n", NULL);
exit(rc);
}
int main(int argc, char *argv[]) {
prog = argv[0];
if (!prog) prog = "verynice";
int opt;
while ((opt = getopt(argc, argv, "h")) != -1) {
switch (opt) {
case 'h':
PrintUsage(0, 1);
default:
PrintUsage(1, 2);
}
}
if (optind == argc) {
tinyprint(2, prog, ": missing command\n", NULL);
exit(1);
}
verynice();
execvp(argv[optind], argv + optind);
perror(argv[optind]);
exit(127);
}