cosmopolitan/libc/dlopen/dlopen.c
2023-11-04 20:33:29 -07:00

581 lines
17 KiB
C

/*-*- 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/atomic.h"
#include "libc/calls/calls.h"
#include "libc/calls/internal.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/calls/syscall_support-nt.internal.h"
#include "libc/cosmo.h"
#include "libc/dce.h"
#include "libc/dlopen/dlfcn.h"
#include "libc/elf/def.h"
#include "libc/elf/elf.h"
#include "libc/elf/struct/auxv.h"
#include "libc/elf/struct/ehdr.h"
#include "libc/elf/struct/phdr.h"
#include "libc/errno.h"
#include "libc/intrin/bits.h"
#include "libc/intrin/strace.internal.h"
#include "libc/limits.h"
#include "libc/nt/dll.h"
#include "libc/nt/enum/filemapflags.h"
#include "libc/nt/enum/pageflags.h"
#include "libc/nt/errors.h"
#include "libc/nt/memory.h"
#include "libc/nt/runtime.h"
#include "libc/runtime/runtime.h"
#include "libc/runtime/syslib.internal.h"
#include "libc/stdio/stdio.h"
#include "libc/stdio/sysparam.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/auxv.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/errfuns.h"
#include "libc/thread/thread.h"
#include "libc/thread/tls.h"
/**
* @fileoverview Cosmopolitan Dynamic Linker.
*
* Every program built using Cosmopolitan is statically-linked. However
* there are some cases, e.g. GUIs and video drivers, where linking the
* host platform libraries is desirable. So what we do in such cases is
* launch a stub executable using the host platform's libc, and longjmp
* back into this executable. The stub executable passes back to us the
* platform-specific dlopen() implementation, which we shall then wrap.
*
* @kudos jacereda for figuring out how to do this
*/
__static_yoink(".dlopen.x86_64.musl.elf");
__static_yoink(".dlopen.x86_64.glibc.elf");
__static_yoink(".dlopen.x86_64.freebsd.elf");
__static_yoink(".dlopen.aarch64.glibc.elf");
#define XNU_RTLD_LAZY 1
#define XNU_RTLD_NOW 2
#define XNU_RTLD_LOCAL 4
#define XNU_RTLD_GLOBAL 8
#define BEGIN_FOREIGN \
{ \
struct CosmoTib *cosmo_tib = __get_tls(); \
pthread_mutex_lock(&foreign.lock); \
__set_tls(foreign.tib)
#define END_FOREIGN \
__set_tls(cosmo_tib); \
pthread_mutex_unlock(&foreign.lock); \
}
struct loaded {
char *base;
char *entry;
Elf64_Ehdr eh;
Elf64_Phdr ph[32];
};
static struct {
atomic_uint once;
bool is_supported;
struct CosmoTib *tib;
pthread_mutex_t lock;
void *(*dlopen)(const char *, int);
void *(*dlsym)(void *, const char *);
int (*dlclose)(void *);
char *(*dlerror)(void);
jmp_buf jb;
} foreign;
static _Thread_local const char *last_dlerror;
static const unsigned align_mask = 4095;
static uintptr_t pgtrunc(uintptr_t x) {
return x & ~align_mask;
}
static uintptr_t pground(uintptr_t x) {
return pgtrunc(x + align_mask);
}
static unsigned pflags(unsigned x) {
unsigned r = 0;
if (x & PF_R) r += PROT_READ;
if (x & PF_W) r += PROT_WRITE;
if (x & PF_X) r += PROT_EXEC;
return r;
}
static char *elf_map(int fd, Elf64_Ehdr *ehdr, Elf64_Phdr *phdr) {
uintptr_t minva = -1;
uintptr_t maxva = 0;
for (Elf64_Phdr *p = phdr; p < &phdr[ehdr->e_phnum]; p++) {
if (p->p_type != PT_LOAD) continue;
if (p->p_vaddr < minva) minva = p->p_vaddr;
if (p->p_vaddr + p->p_memsz > maxva) maxva = p->p_vaddr + p->p_memsz;
}
minva = pgtrunc(minva);
maxva = pground(maxva);
uint8_t *base = __sys_mmap(0, maxva - minva, PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0, 0);
if (base == MAP_FAILED) return MAP_FAILED;
__sys_munmap(base, maxva - minva);
for (Elf64_Phdr *p = phdr; p < &phdr[ehdr->e_phnum]; p++) {
if (p->p_type != PT_LOAD) continue;
uintptr_t off = p->p_vaddr & align_mask;
uint8_t *start = base;
start += pgtrunc(p->p_vaddr);
size_t sz = pground(p->p_memsz + off);
uint8_t *m = __sys_mmap(start, sz, PROT_WRITE,
MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0, 0);
if (m == MAP_FAILED) return MAP_FAILED;
ssize_t rr = pread(fd, m + off, p->p_filesz, p->p_offset);
if (rr != (ssize_t)p->p_filesz) return MAP_FAILED;
sys_mprotect(m, sz, pflags(p->p_flags));
}
return (void *)base;
}
static int elf_open(const char *file) {
return open(file, O_RDONLY | O_CLOEXEC);
}
static bool elf_slurp(struct loaded *l, int fd, const char *file) {
if (pread(fd, &l->eh, 64, 0) != 64) return false;
if (!IsElf64Binary(&l->eh, 64)) return false;
if (l->eh.e_phnum > ARRAYLEN(l->ph)) return false;
int bytes = l->eh.e_phnum * sizeof(l->ph[0]);
if (pread(fd, l->ph, bytes, l->eh.e_phoff) != bytes) return false;
l->entry = (char *)l->eh.e_entry;
return true;
}
static bool elf_load(struct loaded *l, const char *file) {
int fd;
if ((fd = elf_open(file)) == -1) return false;
if (!elf_slurp(l, fd, file)) {
close(fd);
return false;
}
if ((l->base = elf_map(fd, &l->eh, l->ph)) == MAP_FAILED) {
close(fd);
return false;
}
l->entry += (uintptr_t)l->base;
close(fd);
return true;
}
static bool elf_interp(char *buf, size_t bsz, const char *file) {
int fd;
if ((fd = elf_open(file)) == -1) return false;
struct loaded l;
if (!elf_slurp(&l, fd, file)) {
close(fd);
return false;
}
for (unsigned i = 0; i < l.eh.e_phnum; i++) {
if (l.ph[i].p_type == PT_INTERP) {
if (l.ph[i].p_filesz >= bsz ||
pread(fd, buf, l.ph[i].p_filesz, l.ph[i].p_offset) !=
l.ph[i].p_filesz) {
close(fd);
return false;
}
break;
}
}
close(fd);
return true;
}
static long *push_strs(long *sp, char **list, int count) {
*--sp = 0;
while (count) *--sp = (long)list[--count];
return sp;
}
static void elf_exec(const char *file, const char *iinterp, int argc,
char **argv, char **envp) {
// get elf information
struct loaded prog;
if (!elf_load(&prog, file)) return;
struct loaded interp;
if (!elf_load(&interp, iinterp)) return;
// count environment variables
int envc = 0;
while (envp[envc]) envc++;
// create a stack for the new process just beneath our stack
long *sp = (long *)__builtin_frame_address(0) - 256;
// push auxiliary values
*--sp = 0;
Elf64_auxv_t *av;
unsigned long key, val;
for (av = (Elf64_auxv_t *)__auxv; (key = av->a_type); ++av) {
val = av->a_un.a_val;
if (key == AT_PHDR) val = (long)(prog.base + prog.eh.e_phoff);
if (key == AT_PHENT) val = prog.eh.e_phentsize;
if (key == AT_PHNUM) val = prog.eh.e_phnum;
if (key == AT_PAGESZ) val = 0x1000;
if (key == AT_BASE) val = (long)interp.base;
if (key == AT_FLAGS) val = 0;
if (key == AT_ENTRY) val = (long)prog.entry;
if (key == AT_EXECFN) val = (long)argv[0];
*--sp = val;
*--sp = key;
}
// push main() arguments
sp = push_strs(sp, envp, envc);
sp = push_strs(sp, argv, argc);
*--sp = argc;
#ifdef __x86_64__
struct ps_strings {
char **argv;
int argc;
char **envp;
int envc;
} pss = {argv, argc, envp, envc};
asm volatile("mov\t%2,%%rsp\n\t"
"jmpq\t*%1"
: /* no outputs */
: "D"(IsFreebsd() ? sp : 0), "S"(interp.entry), "d"(sp),
"b"(IsNetbsd() ? &pss : 0)
: "memory");
__builtin_unreachable();
#elif defined(__aarch64__)
register long x0 asm("x0") = IsFreebsd() ? (long)sp : 0;
register long x9 asm("x9") = (long)sp;
register long x16 asm("x16") = (long)interp.entry;
asm volatile("mov\tsp,x9\n\t"
"br\tx16"
: /* no outputs */
: "r"(x0), "r"(x9), "r"(x16)
: "memory");
__builtin_unreachable();
#else
#error "unsupported architecture"
#endif
}
static wontreturn void foreign_helper(void **p) {
foreign.dlopen = p[0];
foreign.dlsym = p[1];
foreign.dlclose = p[2];
foreign.dlerror = p[3];
longjmp(foreign.jb, 1);
}
static void foreign_setup(void) {
char interp[256] = {0};
if (!elf_interp(interp, sizeof(interp), "/usr/bin/env")) return;
const char *dlopen_helper = 0;
#ifdef __x86_64__
if (IsFreebsd()) {
dlopen_helper = "/zip/.dlopen.x86_64.freebsd.elf";
} else if (IsLinux()) {
if (fileexists("/lib64/ld-linux-x86-64.so.2")) {
dlopen_helper = "/zip/.dlopen.x86_64.glibc.elf";
} else {
dlopen_helper = "/zip/.dlopen.x86_64.musl.elf";
}
}
#elif defined(__aarch64__)
if (IsLinux()) {
dlopen_helper = "/zip/.dlopen.aarch64.glibc.elf";
}
#endif
if (!dlopen_helper) {
return; // this platform isn't supported yet
}
struct CosmoTib *cosmo_tib = __get_tls();
if (!setjmp(foreign.jb)) {
elf_exec(dlopen_helper, interp, 2,
(char *[]){
program_invocation_name,
(char *)foreign_helper,
NULL,
},
environ);
return; // if it returns then it failed
}
foreign.tib = __get_tls();
__set_tls(cosmo_tib);
foreign.is_supported = true;
}
static bool foreign_init(void) {
bool res;
cosmo_once(&foreign.once, foreign_setup);
if (!(res = foreign.is_supported)) {
last_dlerror = "dlopen() isn't supported on this platform";
}
return res;
}
static int dlclose_nt(void *handle) {
int res;
if (FreeLibrary((uintptr_t)handle)) {
res = 0;
} else {
last_dlerror = "FreeLibrary() failed";
res = -1;
}
return res;
}
static void *dlopen_nt(const char *path, int mode) {
int n;
uintptr_t handle;
char16_t path16[PATH_MAX + 2];
if (mode & ~(RTLD_LOCAL | RTLD_LAZY | RTLD_NOW)) {
last_dlerror = "invalid mode";
return 0;
}
if ((n = __mkntpath(path, path16)) == -1) {
last_dlerror = "path invalid";
return 0;
}
if (n > 3 && //
path16[n - 3] == '.' && //
path16[n - 2] == 's' && //
path16[n - 1] == 'o') {
path16[n - 2] = 'd';
path16[n - 1] = 'l';
path16[n + 0] = 'l';
path16[n + 1] = 0;
}
if (!(handle = LoadLibrary(path16))) {
last_dlerror = "library not found";
}
return (void *)handle;
}
static char *dlsym_nt_alloc_rwx_block(void) {
uintptr_t h;
char *p = 0;
if ((h = CreateFileMapping(-1, 0, kNtPageExecuteReadwrite, 0, 65536, 0)) &&
(p = MapViewOfFileEx(h, kNtFileMapWrite | kNtFileMapExecute, 0, 0, 65536,
0))) {
WRITE32LE(p, 4); // store used index
} else {
last_dlerror = "out of memory";
}
return p;
}
static void *dlsym_nt_alloc_rwx(size_t n) {
void *res;
static char *block;
pthread_mutex_lock(&foreign.lock);
if (!block || READ32LE(block) + n > 65536) {
if (!(block = dlsym_nt_alloc_rwx_block())) {
return 0;
}
}
res = block + READ32LE(block);
WRITE32LE(block, READ32LE(block) + n);
pthread_mutex_unlock(&foreign.lock);
return res;
}
static void *dlsym_nt_thunk(void *func, void *tramp) {
unsigned char *code;
if (!(code = dlsym_nt_alloc_rwx(27))) return 0;
// push %rbp
code[0] = 0x55;
// mov %rsp,%rbp
code[1] = 0x48;
code[2] = 0x89;
code[3] = 0xe5;
// movabs $func,%rax
code[4] = 0x48;
code[5] = 0xb8;
WRITE64LE(code + 6, (uintptr_t)func);
// movabs $tramp,%r10
code[14] = 0x49;
code[15] = 0xba;
WRITE64LE(code + 16, (uintptr_t)tramp);
// jmp *%r10
code[24] = 0x41;
code[25] = 0xff;
code[26] = 0xe2;
return code;
}
static void *dlsym_nt(void *handle, const char *name) {
long __sysv2nt14();
void *x64_abi_func;
void *sysv_abi_func = 0;
if ((x64_abi_func = GetProcAddress((uintptr_t)handle, name))) {
sysv_abi_func = dlsym_nt_thunk(x64_abi_func, __sysv2nt14);
} else {
last_dlerror = "symbol not found";
}
return sysv_abi_func;
}
static void *dlopen_silicon(const char *path, int mode) {
int n;
int xnu_mode = 0;
char path2[PATH_MAX + 5];
if (mode & ~(RTLD_LOCAL | RTLD_LAZY | RTLD_NOW | RTLD_GLOBAL)) {
xnu_mode = -1; // punt error to system dlerror() impl
}
if (!(mode & RTLD_GLOBAL)) {
xnu_mode |= XNU_RTLD_LOCAL; // unlike Linux, XNU defaults to RTLD_GLOBAL
}
if (mode & RTLD_NOW) {
xnu_mode |= XNU_RTLD_NOW;
}
if (mode & RTLD_LAZY) {
xnu_mode |= XNU_RTLD_LAZY;
}
if ((n = strlen(path)) < PATH_MAX && n > 3 && //
path[n - 3] == '.' && //
path[n - 2] == 's' && //
path[n - 1] == 'o') {
memcpy(path2, path, n);
path2[n - 2] = 'd';
path2[n - 1] = 'y';
path2[n + 0] = 'l';
path2[n + 1] = 'i';
path2[n + 2] = 'b';
path2[n + 3] = 0;
path = path2;
}
return __syslib->__dlopen(path, xnu_mode);
}
/**
* Opens dynamic shared object using host platform libc.
*
* If a `path` ending with `.so` is passed on Windows or MacOS, then
* this wrapper will automatically change it to `.dll` or `.dylib` to
* increase its chance of successfully loading.
*
* @param mode is a bitmask that can contain:
* - `RTLD_LOCAL` (default)
* - `RTLD_GLOBAL` (not supported on Windows)
* - `RTLD_LAZY`
* - `RTLD_NOW`
* @return dso handle, or NULL w/ dlerror()
*/
void *dlopen(const char *path, int mode) {
void *res;
if (IsWindows()) {
res = dlopen_nt(path, mode);
} else if (IsXnuSilicon()) {
res = dlopen_silicon(path, mode);
} else if (IsXnu()) {
last_dlerror = "dlopen() isn't supported on x86-64 MacOS";
res = 0;
} else if (foreign_init()) {
BEGIN_FOREIGN;
res = foreign.dlopen(path, mode);
END_FOREIGN;
} else {
res = 0;
}
STRACE("dlopen(%#s, %d) → %p", path, mode, res);
return res;
}
/**
* Obtains address of symbol from dynamic shared object.
*
* On Windows you can only use this to lookup function addresses.
* Returned functions are trampolined to conform to System V ABI.
*
* @param handle was opened by dlopen()
* @return address of symbol, or NULL w/ dlerror()
*/
void *dlsym(void *handle, const char *name) {
void *res;
if (IsWindows()) {
res = dlsym_nt(handle, name);
} else if (IsXnuSilicon()) {
res = __syslib->__dlsym(handle, name);
} else if (IsXnu()) {
last_dlerror = "dlopen() isn't supported on x86-64 MacOS";
res = 0;
} else if (foreign_init()) {
BEGIN_FOREIGN;
res = foreign.dlsym(handle, name);
END_FOREIGN;
} else {
res = 0;
}
STRACE("dlsym(%p, %#s) → %p", handle, name, res);
return res;
}
/**
* Closes dynamic shared object.
*
* @param handle was opened by dlopen()
* @return 0 on success, or -1 w/ dlerror()
*/
int dlclose(void *handle) {
int res;
if (IsWindows()) {
res = dlclose_nt(handle);
} else if (IsXnuSilicon()) {
res = __syslib->__dlclose(handle);
} else if (IsXnu()) {
last_dlerror = "dlopen() isn't supported on x86-64 MacOS";
res = -1;
} else if (foreign_init()) {
BEGIN_FOREIGN;
res = foreign.dlclose(handle);
END_FOREIGN;
} else {
res = -1;
}
STRACE("dlclose(%p) → %d", handle, res);
return res;
}
/**
* Returns string describing last dlopen/dlsym/dlclose error.
*/
char *dlerror(void) {
char *res;
if (IsXnuSilicon()) {
res = __syslib->__dlerror();
} else if (IsWindows() || IsXnu()) {
res = (char *)last_dlerror;
} else if (foreign_init()) {
BEGIN_FOREIGN;
res = foreign.dlerror();
END_FOREIGN;
} else {
res = (char *)last_dlerror;
}
STRACE("dlerror() → %#s", res);
return res;
}