Skip to content

Commit

Permalink
Loader path security (#1012)
Browse files Browse the repository at this point in the history
The ape loader now passes the program executable name directly as a
register. `x2` is used on aarch64, `%rdx` on x86_64. This is passed
as the third argument to `cosmo()` (M1) or `Launch` (non-M1) and is
assigned to the global `__program_executable_name`.

`GetProgramExecutableName` now returns this global's value, setting
it if it is initially null. `InitProgramExecutableName` first tries
exotic, secure methods: `KERN_PROC_PATHNAME` on FreeBSD/NetBSD, and
`/proc` on Linux. If those produce a reasonable response (i.e., not
`"/usr/bin/ape"`, which happens with the loader before this change),
that is used. Otherwise, if `issetugid()`, the empty string is used.
Otherwise, the old argv/envp parsing code is run.

The value returned from the loader is always the full absolute path
of the binary to be executed, having passed through `realpath`. For
the non-M1 loader, this necessitated writing `RealPath`, which uses
`readlinkat` of `"/proc/self/fd/[progfd]"` on Linux, `F_GETPATH` on
Xnu, and the `__realpath` syscall on OpenBSD. On FreeBSD/NetBSD, it
punts to `GetProgramExecutableName`, which is secure on those OSes.

With the loader, all platforms now have a secure program executable
name. With no loader or an old loader, everything still works as it
did, but setuid/setgid is not supported if the insecure pathfinding
code would have been needed.

Fixes #991.
  • Loading branch information
mrdomino authored Dec 15, 2023
1 parent 8a10ccf commit f94c11d
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 136 deletions.
51 changes: 17 additions & 34 deletions ape/ape-m1.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
#include <unistd.h>

#define pagesz 16384
#define VARNAME "COSMOPOLITAN_PROGRAM_EXECUTABLE="
#define VARSIZE (sizeof(VARNAME) - 1)
/* maximum path size that cosmo can take */
#define PATHSIZE (PATH_MAX < 1024 ? PATH_MAX : 1024)
#define SYSLIB_MAGIC ('s' | 'l' << 8 | 'i' << 16 | 'b' << 24)
Expand Down Expand Up @@ -203,11 +201,8 @@ struct PathSearcher {
unsigned long namelen;
const char *name;
const char *syspath;
char varname[VARSIZE];
char path[PATHSIZE];
};
_Static_assert(offsetof(struct PathSearcher, varname) + VARSIZE ==
offsetof(struct PathSearcher, path), "struct layout");

struct ApeLoader {
struct PathSearcher ps;
Expand Down Expand Up @@ -321,17 +316,21 @@ __attribute__((__noreturn__)) static void Pexit(const char *c, int failed,
}

static char AccessCommand(struct PathSearcher *ps, unsigned long pathlen) {
if (!pathlen && *ps->name != '/') {
if (!getcwd(ps->path, sizeof(ps->path) - 1 - ps->namelen)) {
Pexit("getcwd", -errno, "failed");
}
pathlen = strlen(ps->path);
} else if (pathlen + 1 + ps->namelen + 1 > sizeof(ps->path)) {
char buf[PATH_MAX];
size_t n;
if (pathlen + 1 + ps->namelen + 1 > sizeof(ps->path)) {
return 0;
}
if (pathlen && ps->path[pathlen - 1] != '/') ps->path[pathlen++] = '/';
memmove(ps->path + pathlen, ps->name, ps->namelen);
ps->path[pathlen + ps->namelen] = 0;
if (!realpath(ps->path, buf)) {
Pexit(ps->path, -errno, "realpath");
}
if ((n = strlen(buf)) >= sizeof(ps->path)) {
Pexit(buf, 0, "too long");
}
memcpy(ps->path, buf, n + 1);
if (!access(ps->path, X_OK)) {
if (ps->indirect) {
ps->namelen -= 4;
Expand Down Expand Up @@ -563,7 +562,8 @@ static long sys_pselect(int nfds, fd_set *readfds, fd_set *writefds,
__attribute__((__noreturn__)) static void Spawn(const char *exe, int fd,
long *sp, struct ElfEhdr *e,
struct ElfPhdr *p,
struct Syslib *lib) {
struct Syslib *lib,
char *path) {
long rc;
int prot;
int flags;
Expand Down Expand Up @@ -734,10 +734,10 @@ __attribute__((__noreturn__)) static void Spawn(const char *exe, int fd,
close(fd);

register long *x0 __asm__("x0") = sp;
register char *x2 __asm__("x2") = path;
register struct Syslib *x15 __asm__("x15") = lib;
register long x16 __asm__("x16") = e->e_entry;
__asm__ volatile("mov\tx1,#0\n\t"
"mov\tx2,#0\n\t"
"mov\tx3,#0\n\t"
"mov\tx4,#0\n\t"
"mov\tx5,#0\n\t"
Expand Down Expand Up @@ -767,7 +767,7 @@ __attribute__((__noreturn__)) static void Spawn(const char *exe, int fd,
"mov\tx0,#0\n\t"
"br\tx16"
: /* no outputs */
: "r"(x0), "r"(x15), "r"(x16)
: "r"(x0), "r"(x2), "r"(x15), "r"(x16)
: "memory");
__builtin_unreachable();
}
Expand Down Expand Up @@ -891,7 +891,7 @@ static const char *TryElf(struct ApeLoader *M, union ElfEhdrBuf *ebuf,
auxv[28] = 0;

/* we're now ready to load */
Spawn(exe, fd, sp, e, p, &M->lib);
Spawn(exe, fd, sp, e, p, &M->lib, M->ps.path);
}

int main(int argc, char **argv, char **envp) {
Expand All @@ -900,8 +900,7 @@ int main(int argc, char **argv, char **envp) {
struct ApeLoader *M;
long *sp, *sp2, *auxv;
union ElfEhdrBuf *ebuf;
char *p, *pe, *exe, *prog,
*execfn, *shell, **varpos;
char *p, *pe, *exe, *prog, *execfn, *shell;

/* allocate loader memory in program's arg block */
n = sizeof(struct ApeLoader);
Expand Down Expand Up @@ -965,13 +964,9 @@ int main(int argc, char **argv, char **envp) {

/* getenv("_") is close enough to at_execfn */
execfn = argc > 0 ? argv[0] : 0;
varpos = 0;
for (i = 0; envp[i]; ++i) {
if (envp[i][0] == '_' && envp[i][1] == '=') {
execfn = envp[i] + 2;
} else if (!memcmp(VARNAME, envp[i], VARSIZE)) {
assert(!varpos);
varpos = envp + i;
}
}

Expand All @@ -982,7 +977,7 @@ int main(int argc, char **argv, char **envp) {
/* create new bottom of stack for spawned program
system v abi aligns this on a 16-byte boundary
grows down the alloc by poking the guard pages */
n = (auxv - sp + !varpos + AUXV_WORDS + 1) * sizeof(long);
n = (auxv - sp + AUXV_WORDS + 1) * sizeof(long);
sp2 = (long *)__builtin_alloca(n);
if ((long)sp2 & 15) ++sp2;
for (; n > 0; n -= pagesz) {
Expand All @@ -991,12 +986,6 @@ int main(int argc, char **argv, char **envp) {
memmove(sp2, sp, (auxv - sp) * sizeof(long));
argv = (char **)(sp2 + 1);
envp = (char **)(sp2 + 1 + argc + 1);
if (varpos) {
varpos = (char **)((long *)varpos - sp + sp2);
} else {
varpos = envp + i++;
*(envp + i) = 0;
}
auxv = (long *)(envp + i + 1);
sp = sp2;

Expand Down Expand Up @@ -1061,12 +1050,6 @@ int main(int argc, char **argv, char **envp) {
}
pe = ebuf->buf + rc;

/* inject program executable as first environment variable,
swapping the old first variable for it. */
memmove(M->ps.varname, VARNAME, VARSIZE);
*varpos = *envp;
*envp = M->ps.varname;

/* generate some hard random data */
if ((rc = sys_getentropy(M->rando, sizeof(M->rando))) < 0) {
Pexit(argv[0], rc, "getentropy");
Expand Down
13 changes: 6 additions & 7 deletions ape/launch.S
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,16 @@
//
// @param rdi is passed through as-is
// @param rsi is address of entrypoint (becomes zero)
// @param rdx is stack pointer (becomes zero)
// @param rcx is passed through as-is
// @param rdx is passed through as-is
// @param rcx is stack pointer (becomes r8)
// @noreturn
Launch:
#ifdef __aarch64__

mov x16,x1
mov sp,x2
mov sp,x3
mov x1,0
mov x2,0
mov x3,0
mov x3,x4
mov x4,0
mov x5,0
mov x6,0
Expand Down Expand Up @@ -71,6 +70,8 @@ Launch:

#else

mov %rcx,%rsp
mov %r8,%rcx
xor %r8d,%r8d
xor %r9d,%r9d
xor %r10d,%r10d
Expand All @@ -79,8 +80,6 @@ Launch:
xor %r13d,%r13d
xor %r14d,%r14d
xor %r15d,%r15d
mov %rdx,%rsp
xor %edx,%edx
push %rsi
xor %esi,%esi
xor %ebp,%ebp
Expand Down
88 changes: 65 additions & 23 deletions ape/loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
#define MIN(X, Y) ((Y) > (X) ? (X) : (Y))
#define MAX(X, Y) ((Y) < (X) ? (X) : (Y))

#define PATH_MAX 1024 /* XXX verify */

#define SupportsLinux() (SUPPORT_VECTOR & LINUX)
#define SupportsXnu() (SUPPORT_VECTOR & XNU)
#define SupportsFreebsd() (SUPPORT_VECTOR & FREEBSD)
Expand Down Expand Up @@ -212,17 +214,18 @@ struct PathSearcher {
const char *name;
const char *syspath;
unsigned long namelen;
char path[1024];
char path[PATH_MAX];
};

struct ApeLoader {
union ElfPhdrBuf phdr;
struct PathSearcher ps;
char path[1024];
char path[PATH_MAX];
};

EXTERN_C long SystemCall(long, long, long, long, long, long, long, int);
EXTERN_C void Launch(void *, long, void *, int) __attribute__((__noreturn__));
EXTERN_C void
Launch(void *, long, void *, void *, int) __attribute__((__noreturn__));

extern char __executable_start[];
extern char _end[];
Expand All @@ -239,19 +242,27 @@ static int StrCmp(const char *l, const char *r) {
return (l[i] & 255) - (r[i] & 255);
}

static const char *BaseName(const char *s) {
int c;
const char *b = "";
#if 0

static const char *StrRChr(const char *s, int c) {
const char *b = 0;
if (s) {
while ((c = *s++)) {
if (c == '/') {
for (; *s; ++s) {
if (*s == c) {
b = s;
}
}
}
return b;
}

static const char *BaseName(const char *s) {
const char *b = StrRChr(s, '/');
return b ? b + 1 : s;
}

#endif

static void Bzero(void *a, unsigned long n) {
long z;
char *p, *e;
Expand Down Expand Up @@ -343,7 +354,7 @@ static char *Utox(char p[19], unsigned long x) {
return p;
}

static char *Utoa(char p[21], unsigned long x) {
static char *Utoa(char p[20], unsigned long x) {
char t;
unsigned long i, a, b;
i = 0;
Expand Down Expand Up @@ -534,6 +545,40 @@ __attribute__((__noreturn__)) static void Pexit(int os, const char *c, int rc,
Exit(127, os);
}

#define PSFD "/proc/self/fd/"

static int RealPath(int os, int fd, char *path, char **resolved) {
char buf[PATH_MAX];
int rc;
if (IsLinux()) {
char psfd[sizeof(PSFD) + 19];
MemMove(psfd, PSFD, sizeof(PSFD) - 1);
Utoa(psfd + sizeof(PSFD) - 1, fd);
rc = SystemCall(-100, (long)psfd, (long)buf, PATH_MAX, 0, 0, 0,
IsAarch64() ? 78 : 267);
if (rc >= 0) {
if (rc == PATH_MAX) {
rc = -36;
} else {
buf[rc] = 0;
}
}
} else if (IsXnu()) {
rc = SystemCall(fd, 50, (long)buf, 0, 0, 0, 0, 92 | 0x2000000);
} else if (IsOpenbsd()) {
rc = SystemCall((long)path, (long)buf, 0, 0, 0, 0, 0, 115);
} else {
*resolved = 0;
return 0;
}
if (rc >= 0) {
MemMove(path, buf, StrLen(buf) + 1);
*resolved = path;
rc = 0;
}
return rc;
}

static char AccessCommand(struct PathSearcher *ps, unsigned long pathlen) {
if (pathlen + 1 + ps->namelen + 1 > sizeof(ps->path)) return 0;
if (pathlen && ps->path[pathlen - 1] != '/') ps->path[pathlen++] = '/';
Expand Down Expand Up @@ -599,8 +644,9 @@ static char *Commandv(struct PathSearcher *ps, int os, const char *name,
}
}

__attribute__((__noreturn__)) static void Spawn(int os, const char *exe, int fd,
long *sp, unsigned long pagesz,
__attribute__((__noreturn__)) static void Spawn(int os, const char *exe,
char *path, int fd, long *sp,
unsigned long pagesz,
struct ElfEhdr *e,
struct ElfPhdr *p) {
long rc;
Expand Down Expand Up @@ -757,12 +803,12 @@ __attribute__((__noreturn__)) static void Spawn(int os, const char *exe, int fd,
Msyscall(dynbase + code, codesize, os);

/* call program entrypoint */
Launch(IsFreebsd() ? sp : 0, dynbase + e->e_entry, sp, os);
Launch(IsFreebsd() ? sp : 0, dynbase + e->e_entry, path, sp, os);
}

static const char *TryElf(struct ApeLoader *M, union ElfEhdrBuf *ebuf,
const char *exe, int fd, long *sp, long *auxv,
unsigned long pagesz, int os) {
const char *exe, char *path, int fd, long *sp,
long *auxv, unsigned long pagesz, int os) {
long i, rc;
unsigned size;
struct ElfEhdr *e;
Expand Down Expand Up @@ -877,7 +923,7 @@ static const char *TryElf(struct ApeLoader *M, union ElfEhdrBuf *ebuf,
}

/* we're now ready to load */
Spawn(os, exe, fd, sp, pagesz, e, p);
Spawn(os, exe, path, fd, sp, pagesz, e, p);
}

__attribute__((__noreturn__)) static void ShowUsage(int os, int fd, int rc) {
Expand Down Expand Up @@ -1035,19 +1081,15 @@ EXTERN_C __attribute__((__noreturn__)) void ApeLoader(long di, long *sp,
Pexit(os, prog, 0, "not found (maybe chmod +x or ./ needed)");
} else if ((fd = Open(exe, O_RDONLY, 0, os)) < 0) {
Pexit(os, exe, fd, "open");
} else if ((rc = RealPath(os, fd, exe, &prog)) < 0) {
Pexit(os, exe, rc, "realpath");
} else if ((rc = Pread(fd, ebuf->buf, sizeof(ebuf->buf), 0, os)) < 0) {
Pexit(os, exe, rc, "read");
} else if ((unsigned long)rc < sizeof(ebuf->ehdr)) {
Pexit(os, exe, 0, "too small");
}
pe = ebuf->buf + rc;

/* change argv[0] to resolved path if it's ambiguous */
if (argc > 0 && ((*prog != '/' && *exe == '/' && !StrCmp(prog, argv[0])) ||
!StrCmp(BaseName(prog), argv[0]))) {
argv[0] = exe;
}

/* ape intended behavior
1. if ape, will scan shell script for elf printf statements
2. shell script may have multiple lines producing elf headers
Expand Down Expand Up @@ -1080,9 +1122,9 @@ EXTERN_C __attribute__((__noreturn__)) void ApeLoader(long di, long *sp,
}
}
if (i >= sizeof(ebuf->ehdr)) {
TryElf(M, ebuf, exe, fd, sp, auxv, pagesz, os);
TryElf(M, ebuf, exe, prog, fd, sp, auxv, pagesz, os);
}
}
}
Pexit(os, exe, 0, TryElf(M, ebuf, exe, fd, sp, auxv, pagesz, os));
Pexit(os, exe, 0, TryElf(M, ebuf, exe, prog, fd, sp, auxv, pagesz, os));
}
4 changes: 2 additions & 2 deletions examples/env.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
#include "libc/runtime/runtime.h"

int main(int argc, char* argv[]) {
printf("%s\n", argv[0]);
fprintf(stderr, "%s (%s)\n", argv[0], GetProgramExecutableName());
for (char **p = environ; *p; ++p) {
printf(" %s\n", *p);
printf("%s\n", *p);
}
return 0;
}
Loading

0 comments on commit f94c11d

Please sign in to comment.