Loader path security (#1012)

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.
This commit is contained in:
Jōshin 2023-12-15 12:23:58 -05:00 committed by GitHub
parent 8a10ccf9c4
commit f94c11d978
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 193 additions and 137 deletions

View file

@ -51,7 +51,14 @@ static inline int IsAlpha(int c) {
}
static inline void InitProgramExecutableNameImpl(void) {
size_t n;
ssize_t got;
char c, *q, *b;
if (__program_executable_name) {
/* already set by the loader */
return;
}
if (IsWindows()) {
int n = GetModuleFileName(0, g_prog.u.buf16, ARRAYLEN(g_prog.u.buf16));
for (int i = 0; i < n; ++i) {
@ -69,21 +76,44 @@ static inline void InitProgramExecutableNameImpl(void) {
g_prog.u.buf16[2] = '/';
}
tprecode16to8(g_prog.u.buf, sizeof(g_prog.u.buf), g_prog.u.buf16);
return;
goto UseBuf;
}
char c, *q;
if (IsMetal()) {
q = APE_COM_NAME;
goto CopyString;
__program_executable_name = APE_COM_NAME;
return;
}
/* the new-style loader supplies the full program path as the first
environment variable. in the spirit of Postel's Law ("be liberal
in what you accept"), we use __getenv to read it. */
if ((q = __getenv(__envp, "COSMOPOLITAN_PROGRAM_EXECUTABLE").s)) {
strlcpy(g_prog.u.buf, q, sizeof(g_prog.u.buf));
return;
b = g_prog.u.buf;
n = sizeof(g_prog.u.buf) - 1;
if (IsFreebsd() || IsNetbsd()) {
int cmd[4];
cmd[0] = CTL_KERN;
cmd[1] = KERN_PROC;
if (IsFreebsd()) {
cmd[2] = KERN_PROC_PATHNAME_FREEBSD;
} else {
cmd[2] = KERN_PROC_PATHNAME_NETBSD;
}
cmd[3] = -1; // current process
if (sys_sysctl(cmd, ARRAYLEN(cmd), b, &n, 0, 0) != -1) {
if (strcmp(b, "/usr/bin/ape")) { // XX old loader; warn?
goto UseBuf;
}
}
}
if (IsLinux()) {
if ((got = sys_readlinkat(AT_FDCWD, "/proc/self/exe", b, n)) > 0 ||
(got = sys_readlinkat(AT_FDCWD, "/proc/curproc/file", b, n)) > 0) {
b[got] = 0;
if (strcmp(b, "/usr/bin/ape")) {
goto UseBuf;
}
}
}
if (issetugid()) {
/* give up prior to using less secure methods */
goto UseEmpty;
}
// if argv[0] exists then turn it into an absolute path. we also try
@ -107,41 +137,17 @@ static inline void InitProgramExecutableNameImpl(void) {
}
}
*p = 0;
if (!sys_faccessat(AT_FDCWD, g_prog.u.buf, F_OK, 0)) return;
if (!sys_faccessat(AT_FDCWD, g_prog.u.buf, F_OK, 0)) goto UseBuf;
p = WRITE32LE(p, READ32LE(".com"));
*p = 0;
if (!sys_faccessat(AT_FDCWD, g_prog.u.buf, F_OK, 0)) return;
if (!sys_faccessat(AT_FDCWD, g_prog.u.buf, F_OK, 0)) goto UseBuf;
}
// if getenv("_") exists then use that
for (char **ep = __envp; (q = *ep); ++ep) {
if (*q++ == '_' && *q++ == '=') {
goto CopyString;
}
}
// if argv[0] doesn't exist, then fallback to interpreter name
ssize_t got;
char *b = g_prog.u.buf;
size_t n = sizeof(g_prog.u.buf) - 1;
if ((got = sys_readlinkat(AT_FDCWD, "/proc/self/exe", b, n)) > 0 ||
(got = sys_readlinkat(AT_FDCWD, "/proc/curproc/file", b, n)) > 0) {
b[got] = 0;
return;
}
if (IsFreebsd() || IsNetbsd()) {
int cmd[4];
cmd[0] = CTL_KERN;
cmd[1] = KERN_PROC;
if (IsFreebsd()) {
cmd[2] = KERN_PROC_PATHNAME_FREEBSD;
} else {
cmd[2] = KERN_PROC_PATHNAME_NETBSD;
}
cmd[3] = -1; // current process
if (sys_sysctl(cmd, ARRAYLEN(cmd), b, &n, 0, 0) != -1) {
return;
}
/* the previous loader supplied the full program path as the first
environment variable. we also try "_". */
if ((q = __getenv(__envp, "COSMOPOLITAN_PROGRAM_EXECUTABLE").s) ||
(q = __getenv(__envp, "_").s)) {
goto CopyString;
}
// give up and just copy argv[0] into it
@ -155,14 +161,18 @@ static inline void InitProgramExecutableNameImpl(void) {
}
}
*p = 0;
return;
goto UseBuf;
}
// if we don't even have that then empty the string
UseEmpty:
g_prog.u.buf[0] = 0;
UseBuf:
__program_executable_name = g_prog.u.buf;
}
void __InitProgramExecutableName(void) {
static void InitProgramExecutableName(void) {
int e = errno;
InitProgramExecutableNameImpl();
errno = e;
@ -172,6 +182,6 @@ void __InitProgramExecutableName(void) {
* Returns absolute path of program.
*/
char *GetProgramExecutableName(void) {
cosmo_once(&g_prog.once, __InitProgramExecutableName);
return g_prog.u.buf;
cosmo_once(&g_prog.once, InitProgramExecutableName);
return __program_executable_name;
}

View file

@ -62,6 +62,8 @@ _start:
// set operating system when already detected
1: mov %cl,__hostos(%rip)
mov %rdx,__program_executable_name(%rip)
// get startup timestamp as early as possible
// its used by --strace flag and kprintf() %T
rdtsc
@ -140,6 +142,8 @@ _start:
// should be set to zero on other platforms
mov x1,x15
// third arg (x2) is the program path passed by ape-m1.c
// switch to c code
bl cosmo
.unreachable

View file

@ -0,0 +1,21 @@
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
vi: set et 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/runtime/runtime.h"
char *__program_executable_name;

View file

@ -78,7 +78,7 @@ static const char *DecodeMagnum(const char *p, long *r) {
return *r = x, p;
}
wontreturn textstartup void cosmo(long *sp, struct Syslib *m1) {
wontreturn textstartup void cosmo(long *sp, struct Syslib *m1, char *exename) {
// get startup timestamp as early as possible
// its used by --strace and also kprintf() %T
@ -108,6 +108,7 @@ wontreturn textstartup void cosmo(long *sp, struct Syslib *m1) {
__envp = envp;
__auxv = auxv;
environ = envp;
__program_executable_name = exename;
program_invocation_name = argv[0];
__oldstack = (intptr_t)sp;

View file

@ -71,6 +71,7 @@ extern char **__argv;
extern char **__envp;
extern unsigned long *__auxv;
extern intptr_t __oldstack;
extern char *__program_executable_name;
extern uint64_t __nosync;
extern int __strace;
extern int __ftrace;