| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in |
| * the documentation and/or other materials provided with the |
| * distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
| * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
| * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
| * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
| * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS |
| * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
| * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT |
| * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| * SUCH DAMAGE. |
| */ |
| |
| #define SYSCALL_NO_TLS 1 |
| #include <elf.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <link.h> |
| #include <stdalign.h> |
| #include <stdarg.h> |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <sys/mman.h> |
| #include <sys/param.h> |
| #include <sys/syscall.h> |
| #include <sys/user.h> |
| #include <unistd.h> |
| |
| #include "reloc.h" |
| #include "syscall.h" |
| |
| typedef void EntryFunc(void); |
| |
| // arm64 doesn't have a constant page size and has to use the value from AT_PAGESZ. |
| #ifndef PAGE_SIZE |
| #define PAGE_SIZE g_page_size |
| #endif |
| |
| #define PAGE_START(x) ((x) & (~(PAGE_SIZE-1))) |
| #define PAGE_END(x) PAGE_START((x) + (PAGE_SIZE - 1)) |
| |
| #define START "_start" |
| #include "crt_arch.h" |
| |
| int main(); |
| weak void _init(); |
| weak void _fini(); |
| int __libc_start_main(int (*)(), int, char **, |
| void (*)(), void(*)(), void(*)()); |
| |
| static ElfW(Phdr) replacement_phdr_table[64]; |
| static char replacement_interp[PATH_MAX]; |
| |
| static bool g_debug = false; |
| static const char* g_prog_name = NULL; |
| static uintptr_t g_page_size = 0; |
| static int g_errno = 0; |
| |
| __attribute__((visibility("hidden"))) extern ElfW(Dyn) _DYNAMIC[]; |
| |
| __attribute__((used)) |
| static long ri_set_errno(unsigned long val) { |
| if (val > -4096UL) { |
| g_errno = -val; |
| return -1; |
| } |
| return val; |
| } |
| |
| #define ri_syscall(...) ri_set_errno(__syscall(__VA_ARGS__)) |
| |
| static ssize_t ri_write(int fd, const void* buf, size_t amt) { |
| return ri_syscall(SYS_write, fd, buf, amt); |
| } |
| |
| __attribute__((noreturn)) |
| static void ri_exit(int status) { |
| ri_syscall(SYS_exit, status); |
| __builtin_unreachable(); |
| } |
| |
| static int ri_open(const char* path, int flags, mode_t mode) { |
| return ri_syscall(SYS_openat, AT_FDCWD, path, flags, mode); |
| } |
| |
| static int ri_close(int fd) { |
| return ri_syscall(SYS_close, fd); |
| } |
| |
| static off_t ri_lseek(int fd, off_t offset, int whence) { |
| return ri_syscall(SYS_lseek, fd, offset, whence); |
| } |
| |
| static ssize_t ri_readlink(const char* path, char* buf, size_t size) { |
| return ri_syscall(SYS_readlinkat, AT_FDCWD, path, buf, size); |
| } |
| |
| static void* ri_mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset) { |
| #ifdef SYS_mmap2 |
| return (void*)ri_syscall(SYS_mmap2, addr, length, prot, flags, fd, offset/SYSCALL_MMAP2_UNIT); |
| #else |
| return (void*)ri_syscall(SYS_mmap, addr, length, prot, flags, fd, offset); |
| #endif |
| } |
| |
| static void* ri_munmap(void* addr, size_t length) { |
| return (void*)ri_syscall(SYS_munmap, addr, length); |
| } |
| |
| static int ri_mprotect(void* addr, size_t len, int prot) { |
| return ri_syscall(SYS_mprotect, addr, len, prot); |
| } |
| |
| static ssize_t ri_pread(int fd, void* buf, size_t size, off_t ofs) { |
| return ri_syscall(SYS_pread, fd, buf, size, __SYSCALL_LL_PRW(ofs)); |
| } |
| |
| static size_t ri_strlen(const char* src) { |
| for (size_t len = 0;; ++len) { |
| if (src[len] == '\0') return len; |
| } |
| } |
| |
| static char* ri_strcpy(char* dst, const char* src) { |
| char* result = dst; |
| while ((*dst = *src) != '\0') { |
| ++dst; |
| ++src; |
| } |
| return result; |
| } |
| |
| static char* ri_strcat(char* dst, const char* src) { |
| ri_strcpy(dst + ri_strlen(dst), src); |
| return dst; |
| } |
| |
| static void* ri_memset(void* dst, int val, size_t len) { |
| for (size_t i = 0; i < len; ++i) { |
| ((char*)dst)[i] = val; |
| } |
| return dst; |
| } |
| |
| __attribute__ ((unused)) |
| static void* ri_memcpy(void* dst, const void* src, size_t len) { |
| for (size_t i = 0; i < len; ++i) { |
| ((char*)dst)[i] = ((char*)src)[i]; |
| } |
| return dst; |
| } |
| |
| static int ri_strncmp(const char* x, const char *y, size_t maxlen) { |
| for (size_t i = 0;; ++i) { |
| if (i == maxlen) return 0; |
| int result = (unsigned char)x[i] - (unsigned char)y[i]; |
| if (result != 0) return result; |
| if (x[i] == '\0') return 0; |
| } |
| } |
| |
| static int ri_strcmp(const char* x, const char *y) { |
| return ri_strncmp(x, y, SIZE_MAX); |
| } |
| |
| static char* ri_strrchr(const char* str, int ch) { |
| char* result = NULL; |
| while (true) { |
| if (*str == ch) result = (char*)str; |
| if (*str == '\0') break; |
| ++str; |
| } |
| return result; |
| } |
| |
| static char* ri_strchr(const char* str, int ch) { |
| while (*str) { |
| if (*str == ch) return (char*)str; |
| ++str; |
| } |
| return NULL; |
| } |
| |
| static void ri_dirname(char* path) { |
| char* last_slash = ri_strrchr(path, '/'); |
| if (last_slash == NULL) { |
| path[0] = '.'; // returns "." |
| path[1] = '\0'; |
| } else if (last_slash == path) { |
| path[1] = '\0'; // returns "/" |
| } else { |
| *last_slash = '\0'; |
| } |
| } |
| |
| static void out_str_n(const char* str, size_t n) { |
| ri_write(STDERR_FILENO, str, n); |
| } |
| |
| static void out_str(const char* str) { |
| out_str_n(str, ri_strlen(str)); |
| } |
| |
| static char* ul_to_str(unsigned long i, char* out, unsigned char base) { |
| char buf[65]; |
| char* cur = &buf[65]; |
| *--cur = '\0'; |
| do { |
| *--cur = "0123456789abcdef"[i % base]; |
| i /= base; |
| } while (i > 0); |
| return ri_strcpy(out, cur); |
| } |
| |
| static char* l_to_str(long i, char* out, unsigned char base) { |
| if (i < 0) { |
| *out = '-'; |
| ul_to_str(-(unsigned long)i, out + 1, base); |
| return out; |
| } else { |
| return ul_to_str(i, out, base); |
| } |
| } |
| |
| static const char* ri_strerror(int err) { |
| switch (err) { |
| case EPERM: return "Operation not permitted"; |
| case ENOENT: return "No such file or directory"; |
| case EIO: return "I/O error"; |
| case ENXIO: return "No such device or address"; |
| case EAGAIN: return "Try again"; |
| case ENOMEM: return "Out of memory"; |
| case EACCES: return "Permission denied"; |
| case ENODEV: return "No such device"; |
| case ENOTDIR: return "Not a directory"; |
| case EINVAL: return "Invalid argument"; |
| case ENFILE: return "File table overflow"; |
| case EMFILE: return "Too many open files"; |
| case ESPIPE: return "Illegal seek"; |
| case ENAMETOOLONG: return "File name too long"; |
| case ELOOP: return "Too many symbolic links encountered"; |
| } |
| static char buf[64]; |
| ri_strcpy(buf, "Unknown error "); |
| l_to_str(err, buf + ri_strlen(buf), 10); |
| return buf; |
| } |
| |
| static void outv(const char *fmt, va_list ap) { |
| char buf[65]; |
| while (true) { |
| if (fmt[0] == '\0') break; |
| |
| #define NUM_FMT(num_fmt, type, func, base) \ |
| if (!ri_strncmp(fmt, num_fmt, sizeof(num_fmt) - 1)) { \ |
| out_str(func(va_arg(ap, type), buf, base)); \ |
| fmt += sizeof(num_fmt) - 1; \ |
| continue; \ |
| } |
| NUM_FMT("%d", int, l_to_str, 10); |
| NUM_FMT("%ld", long, l_to_str, 10); |
| NUM_FMT("%u", unsigned int, ul_to_str, 10); |
| NUM_FMT("%lu", unsigned long, ul_to_str, 10); |
| NUM_FMT("%zu", size_t, ul_to_str, 10); |
| NUM_FMT("%x", unsigned int, ul_to_str, 16); |
| NUM_FMT("%lx", unsigned long, ul_to_str, 16); |
| NUM_FMT("%zx", size_t, ul_to_str, 16); |
| #undef NUM_FMT |
| |
| if (!ri_strncmp(fmt, "%p", 2)) { |
| out_str(ul_to_str((unsigned long)va_arg(ap, void*), buf, 16)); |
| fmt += 2; |
| } else if (!ri_strncmp(fmt, "%s", 2)) { |
| const char* arg = va_arg(ap, const char*); |
| out_str(arg ? arg : "(null)"); |
| fmt += 2; |
| } else if (!ri_strncmp(fmt, "%%", 2)) { |
| out_str("%"); |
| fmt += 2; |
| } else if (fmt[0] == '%') { |
| buf[0] = fmt[1]; |
| buf[1] = '\0'; |
| out_str("relinterp error: unrecognized output specifier: '%"); |
| out_str(buf); |
| out_str("'\n"); |
| ri_exit(1); |
| } else { |
| size_t len = 0; |
| while (fmt[len] != '\0' && fmt[len] != '%') ++len; |
| out_str_n(fmt, len); |
| fmt += len; |
| } |
| } |
| } |
| |
| __attribute__((format(printf, 1, 2))) |
| static void debug(const char* fmt, ...) { |
| if (!g_debug) return; |
| out_str("relinterp: "); |
| |
| va_list ap; |
| va_start(ap, fmt); |
| outv(fmt, ap); |
| va_end(ap); |
| out_str("\n"); |
| } |
| |
| __attribute__((format(printf, 1, 2), noreturn)) |
| static void fatal(const char* fmt, ...) { |
| out_str("relinterp: "); |
| if (g_prog_name) { |
| out_str(g_prog_name); |
| out_str(": "); |
| } |
| out_str("fatal error: "); |
| |
| va_list ap; |
| va_start(ap, fmt); |
| outv(fmt, ap); |
| va_end(ap); |
| out_str("\n"); |
| ri_exit(1); |
| } |
| |
| static void* optimizer_barrier(void* val) { |
| __asm__ volatile ("nop" :: "r"(&val) : "memory"); |
| return val; |
| } |
| |
| typedef struct { |
| unsigned long key; |
| unsigned long value; |
| } AuxEntry; |
| |
| typedef struct { |
| int argc; |
| char **argv; |
| char **envp; |
| size_t envp_count; |
| AuxEntry* auxv; |
| size_t auxv_count; |
| } KernelArguments; |
| |
| static KernelArguments read_args(void* raw_args) { |
| KernelArguments result; |
| result.argc = *(long*)raw_args; |
| result.argv = (char**)((void**)raw_args + 1); |
| result.envp = result.argv + result.argc + 1; |
| |
| char** envp = result.envp; |
| while (*envp != NULL) ++envp; |
| result.envp_count = envp - result.envp; |
| ++envp; |
| |
| result.auxv = (AuxEntry*)envp; |
| size_t count = 0; |
| while (result.auxv[count].key != 0) { |
| ++count; |
| } |
| result.auxv_count = count; |
| return result; |
| } |
| |
| static void dump_auxv(const KernelArguments* args) { |
| for (size_t i = 0; i < args->auxv_count; ++i) { |
| const char* name = ""; |
| switch (args->auxv[i].key) { |
| case AT_BASE: name = " [AT_BASE]"; break; |
| case AT_EGID: name = " [AT_EGID]"; break; |
| case AT_ENTRY: name = " [AT_ENTRY]"; break; |
| case AT_EUID: name = " [AT_EUID]"; break; |
| case AT_GID: name = " [AT_GID]"; break; |
| case AT_PAGESZ: name = " [AT_PAGESZ]"; break; |
| case AT_PHDR: name = " [AT_PHDR]"; break; |
| case AT_PHENT: name = " [AT_PHENT]"; break; |
| case AT_PHNUM: name = " [AT_PHNUM]"; break; |
| case AT_SECURE: name = " [AT_SECURE]"; break; |
| case AT_SYSINFO: name = " [AT_SYSINFO]"; break; |
| case AT_SYSINFO_EHDR: name = " [AT_SYSINFO_EHDR]"; break; |
| case AT_UID: name = " [AT_UID]"; break; |
| } |
| debug(" %lu => 0x%lx%s", args->auxv[i].key, args->auxv[i].value, name); |
| } |
| } |
| |
| static unsigned long ri_getauxval(const KernelArguments* args, unsigned long kind, |
| bool allow_missing) { |
| for (size_t i = 0; i < args->auxv_count; ++i) { |
| if (args->auxv[i].key == kind) return args->auxv[i].value; |
| } |
| if (!allow_missing) fatal("could not find aux vector entry %lu", kind); |
| return 0; |
| } |
| |
| static int elf_flags_to_prot(int flags) { |
| int result = 0; |
| if (flags & PF_R) result |= PROT_READ; |
| if (flags & PF_W) result |= PROT_WRITE; |
| if (flags & PF_X) result |= PROT_EXEC; |
| return result; |
| } |
| |
| typedef struct { |
| int fd; |
| char path[PATH_MAX]; |
| } OpenedLoader; |
| |
| typedef struct { |
| void* base_addr; |
| EntryFunc* entry; |
| } LoadedInterp; |
| |
| static LoadedInterp load_interp(const OpenedLoader *loader, ElfW(Ehdr)* hdr) { |
| ElfW(Phdr)* phdr = (ElfW(Phdr)*)((char*)hdr + hdr->e_phoff); |
| size_t phdr_count = hdr->e_phnum; |
| |
| size_t max_vaddr = 0; |
| |
| // Find the virtual address extent. |
| for (size_t i = 0; i < phdr_count; ++i) { |
| if (phdr[i].p_type == PT_LOAD) { |
| max_vaddr = PAGE_END(MAX(max_vaddr, phdr[i].p_vaddr + phdr[i].p_memsz)); |
| } |
| } |
| |
| // Map an area to fit the loader. |
| void* loader_vaddr = ri_mmap(NULL, max_vaddr, PROT_READ | PROT_WRITE, |
| MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); |
| if (loader_vaddr == (void*)MAP_FAILED) { |
| fatal("reservation mmap of 0x%zx bytes for %s failed: %s", max_vaddr, loader->path, |
| ri_strerror(g_errno)); |
| } |
| |
| // Map each PT_LOAD. |
| for (size_t i = 0; i < phdr_count; ++i) { |
| if (phdr[i].p_type == PT_LOAD) { |
| size_t start = PAGE_START(phdr[i].p_vaddr); |
| const size_t end = PAGE_END(phdr[i].p_vaddr + phdr[i].p_memsz); |
| if (phdr[i].p_filesz > 0) { |
| const size_t file_end = phdr[i].p_vaddr + phdr[i].p_filesz; |
| void* tmp = ri_mmap((char*)loader_vaddr + start, |
| file_end - start, |
| elf_flags_to_prot(phdr[i].p_flags), |
| MAP_PRIVATE | MAP_FIXED, loader->fd, PAGE_START(phdr[i].p_offset)); |
| if (tmp == (void*)MAP_FAILED) { |
| fatal("PT_LOAD mmap failed (%s segment #%zu): %s", loader->path, i, |
| ri_strerror(g_errno)); |
| } |
| start = file_end; |
| if (phdr[i].p_flags & PF_W) { |
| // The bytes between p_filesz and PAGE_END(p_filesz) currently come from the file mapping, |
| // but they need to be zeroed. (Apparently this zeroing isn't necessary if the segment isn't |
| // writable, and zeroing a non-writable page would be inconvenient.) |
| ri_memset((char*)loader_vaddr + start, '\0', PAGE_END(start) - start); |
| } |
| start = PAGE_END(start); |
| } |
| if (start < end) { |
| // The memory is already zeroed, because it comes from an anonymous file mapping. Just set |
| // the protections correctly. |
| int result = ri_mprotect((char*)loader_vaddr + start, end - start, |
| elf_flags_to_prot(phdr[i].p_flags)); |
| if (result != 0) { |
| fatal("mprotect of PT_LOAD failed (%s segment #%zu): %s", loader->path, i, |
| ri_strerror(g_errno)); |
| } |
| } |
| } |
| } |
| |
| return (LoadedInterp) { |
| .base_addr = loader_vaddr, |
| .entry = (EntryFunc*)((uintptr_t)loader_vaddr + hdr->e_entry), |
| }; |
| } |
| |
| typedef struct { |
| ElfW(Phdr)* phdr; |
| size_t phdr_count; |
| uintptr_t load_bias; |
| uintptr_t page_size; |
| char* search_paths; |
| ElfW(Ehdr)* ehdr; |
| ElfW(Phdr)* first_load; |
| bool secure; |
| } ExeInfo; |
| |
| static ExeInfo get_exe_info(const KernelArguments* args) { |
| ExeInfo result = { 0 }; |
| result.phdr = (ElfW(Phdr)*)ri_getauxval(args, AT_PHDR, false); |
| result.phdr_count = ri_getauxval(args, AT_PHNUM, false); |
| result.page_size = ri_getauxval(args, AT_PAGESZ, false); |
| |
| unsigned long uid = ri_getauxval(args, AT_UID, false); |
| unsigned long euid = ri_getauxval(args, AT_EUID, false); |
| unsigned long gid = ri_getauxval(args, AT_GID, false); |
| unsigned long egid = ri_getauxval(args, AT_EGID, false); |
| unsigned long secure = ri_getauxval(args, AT_SECURE, true); |
| result.secure = uid != euid || gid != egid || secure; |
| |
| debug("orig phdr = %p", (void*)result.phdr); |
| debug("orig phnum = %zu", result.phdr_count); |
| |
| for (size_t i = 0; i < result.phdr_count; ++i) { |
| if (result.phdr[i].p_type == PT_DYNAMIC) { |
| result.load_bias = (uintptr_t)&_DYNAMIC - result.phdr[i].p_vaddr; |
| } |
| } |
| debug("load_bias = 0x%lx", (unsigned long)result.load_bias); |
| |
| for (size_t i = 0; i < result.phdr_count; ++i) { |
| ElfW(Phdr)* phdr = &result.phdr[i]; |
| if (phdr->p_type != PT_LOAD) continue; |
| result.first_load = phdr; |
| if (phdr->p_offset != 0) { |
| fatal("expected zero p_offset for first PT_LOAD, found 0x%zx instead", |
| (size_t)phdr->p_offset); |
| } |
| result.ehdr = (ElfW(Ehdr)*)(phdr->p_vaddr + result.load_bias); |
| break; |
| } |
| debug("ehdr = %p", (void*)result.ehdr); |
| |
| ElfW(Word) runpath_offset = -1; |
| char* strtab = NULL; |
| for (ElfW(Dyn)* dyn = _DYNAMIC; dyn->d_tag != DT_NULL; dyn++) { |
| switch (dyn->d_tag) { |
| case DT_RUNPATH: |
| runpath_offset = dyn->d_un.d_val; |
| break; |
| case DT_RPATH: |
| if (runpath_offset == -1) runpath_offset = dyn->d_un.d_val; |
| break; |
| case DT_STRTAB: |
| strtab = (char*)(dyn->d_un.d_ptr + result.load_bias); |
| break; |
| } |
| } |
| |
| if (strtab && runpath_offset != -1) { |
| result.search_paths = strtab + runpath_offset; |
| debug("dt_runpath = %s", result.search_paths); |
| } |
| return result; |
| } |
| |
| // Loaders typically read the PT_INTERP of the executable, e.g. to set a pathname on the loader. |
| // glibc insists on the executable having PT_INTERP, and aborts if it's missing. Musl passes it |
| // to debuggers to find symbols for the loader, which includes all the libc symbols. |
| // |
| // Make a copy of the phdr table and insert PT_INTERP into the copy. |
| // |
| static void insert_pt_interp_into_phdr_table(const KernelArguments* args, const ExeInfo* exe, |
| const char* loader_realpath) { |
| // Reserve extra space for the inserted PT_PHDR and PT_INTERP segments and a null terminator. |
| if (exe->phdr_count + 3 > sizeof(replacement_phdr_table) / sizeof(replacement_phdr_table[0])) { |
| fatal("too many phdr table entries in executable"); |
| } |
| |
| ElfW(Phdr) newPhdr = { |
| .p_type = PT_PHDR, |
| // The replacement phdr is in the BSS section, which has no file location. |
| // Use 0 for the offset. If this causes a problem the replacement phdr could |
| // be moved to the data section and the correct p_offset calculated. |
| .p_offset = 0, |
| .p_vaddr = (uintptr_t)&replacement_phdr_table - exe->load_bias, |
| .p_paddr = (uintptr_t)&replacement_phdr_table - exe->load_bias, |
| .p_memsz = (exe->phdr_count + 1) * sizeof(ElfW(Phdr)), |
| .p_filesz = (exe->phdr_count + 1) * sizeof(ElfW(Phdr)), |
| .p_flags = PF_R, |
| .p_align = alignof(ElfW(Phdr)), |
| }; |
| |
| ElfW(Phdr*) cur = replacement_phdr_table; |
| if (exe->phdr[0].p_type != PT_PHDR) { |
| // ld.bfd does not insert a PT_PHDR if there is no PT_INTERP, fake one. |
| // It has to be first. We're adding an entry so increase memsz and filesz. |
| newPhdr.p_memsz += sizeof(ElfW(Phdr)); |
| newPhdr.p_filesz += sizeof(ElfW(Phdr)); |
| *cur = newPhdr; |
| ++cur; |
| } |
| |
| for (size_t i = 0; i < exe->phdr_count; ++i) { |
| switch (exe->phdr[i].p_type) { |
| case 0: |
| fatal("unexpected null phdr entry at index %zu", i); |
| break; |
| case PT_PHDR: |
| *cur = newPhdr; |
| break; |
| default: |
| *cur = exe->phdr[i]; |
| } |
| ++cur; |
| } |
| |
| // Insert PT_INTERP at the end. |
| cur->p_type = PT_INTERP; |
| cur->p_offset = 0; |
| cur->p_vaddr = (uintptr_t)&replacement_interp - exe->load_bias; |
| cur->p_paddr = cur->p_vaddr; |
| cur->p_filesz = ri_strlen(replacement_interp) + 1; |
| cur->p_memsz = ri_strlen(replacement_interp) + 1; |
| cur->p_flags = PF_R; |
| cur->p_align = 1; |
| ++cur; |
| |
| ri_strcpy(replacement_interp, loader_realpath); |
| |
| debug("new phdr = %p", (void*)&replacement_phdr_table); |
| debug("new phnum = %zu", cur - replacement_phdr_table); |
| |
| // Update the aux vector with the new phdr+phnum. |
| for (size_t i = 0; i < args->auxv_count; ++i) { |
| if (args->auxv[i].key == AT_PHDR) { |
| args->auxv[i].value = (unsigned long)&replacement_phdr_table; |
| } else if (args->auxv[i].key == AT_PHNUM) { |
| args->auxv[i].value = cur - replacement_phdr_table; |
| } |
| } |
| |
| // AT_PHDR and AT_PHNUM are now updated to point to the replacement program |
| // headers, but the e_phoff and e_phnum in the ELF headers still point to the |
| // original program headers. dynlink.c doesn't use e_phoff value from the |
| // main application's program headers. The e_phoff and e_phnum values could |
| // be updated, but that would require using mprotect to allow modifications |
| // to the read-only first page. |
| } |
| |
| static void realpath_fd(int fd, const char* orig_path, char* out, size_t len) { |
| char path[64]; |
| ri_strcpy(path, "/proc/self/fd/"); |
| ul_to_str(fd, path + ri_strlen(path), 10); |
| ssize_t result = ri_readlink(path, out, len); |
| if (result == -1) fatal("could not get realpath of %s: %s", orig_path, ri_strerror(g_errno)); |
| if ((size_t)result >= len) fatal("realpath of %s too long", orig_path); |
| } |
| |
| static int open_loader(const ExeInfo* exe, const char* path, OpenedLoader* loader) { |
| debug("trying to open '%s'", path); |
| loader->fd = ri_open(path, O_RDONLY, 0); |
| if (loader->fd < 0) { |
| debug("could not open loader %s: %s", path, ri_strerror(g_errno)); |
| return -1; |
| } |
| |
| ElfW(Ehdr) hdr; |
| ssize_t l = ri_pread(loader->fd, &hdr, sizeof(hdr), 0); |
| if (l < 0) { |
| debug("reading elf header from %s failed: %s", path, ri_strerror(g_errno)); |
| return -1; |
| } |
| if (l != sizeof(hdr)) { |
| debug("file %s too short to contain elf header", path); |
| return -1; |
| } |
| |
| if (hdr.e_ident[0] != ELFMAG0 || |
| hdr.e_ident[1] != ELFMAG1 || |
| hdr.e_ident[2] != ELFMAG2 || |
| hdr.e_ident[3] != ELFMAG3) { |
| debug("file %s is not an elf file", path); |
| return -1; |
| } |
| |
| if (hdr.e_machine != exe->ehdr->e_machine) { |
| debug("incorrect elf machine for loader %s, expected %d got %d", |
| path, exe->ehdr->e_machine, hdr.e_machine); |
| return -1; |
| } |
| |
| if (hdr.e_ident[EI_CLASS] != exe->ehdr->e_ident[EI_CLASS]) { |
| debug("incorrect elf class for loader %s, expected %d got %d", |
| path, exe->ehdr->e_ident[EI_CLASS], hdr.e_ident[EI_CLASS]); |
| return -1; |
| } |
| |
| realpath_fd(loader->fd, path, loader->path, sizeof(loader->path)); |
| |
| return 0; |
| } |
| |
| static int open_rel_loader(const ExeInfo* exe, const char* dir, const char* rel, OpenedLoader* loader) { |
| char buf[PATH_MAX]; |
| |
| size_t dir_len = ri_strlen(dir); |
| |
| if (dir_len + (dir_len == 0 ? 1 : 0) + ri_strlen(rel) + 2 > sizeof(buf)) { |
| debug("path to loader exceeds PATH_MAX: %s/%s", dir, rel); |
| return 1; |
| } |
| |
| if (dir_len == 0) { |
| ri_strcpy(buf, "."); |
| } else { |
| ri_strcpy(buf, dir); |
| if (dir[dir_len-1] != '/') { |
| ri_strcat(buf, "/"); |
| } |
| } |
| ri_strcat(buf, rel); |
| |
| return open_loader(exe, buf, loader); |
| } |
| |
| static void get_origin(char* buf, size_t buf_len) { |
| ssize_t len = ri_readlink("/proc/self/exe", buf, buf_len); |
| if (len <= 0 || (size_t)len >= buf_len) { |
| fatal("could not readlink /proc/self/exe: %s", ri_strerror(g_errno)); |
| } |
| buf[len] = '\0'; |
| |
| ri_dirname(buf); |
| } |
| |
| static int search_path_list_for_loader(const ExeInfo* exe, const char* loader_rel_path, const char* search_path, |
| const char* search_path_name, bool expand_origin, OpenedLoader *loader) { |
| char origin_buf[PATH_MAX]; |
| char* origin = NULL; |
| |
| const char* p = search_path; |
| while (p && p[0]) { |
| const char* start = p; |
| const char* end = ri_strchr(p, ':'); |
| if (end == NULL) { |
| end = start + ri_strlen(p); |
| p = NULL; |
| } else { |
| p = end + 1; |
| } |
| size_t n = end - start; |
| char search_path_entry[PATH_MAX]; |
| if (n >= sizeof(search_path_entry)) { |
| // Too long, skip. |
| debug("%s entry too long: %s", search_path_name, start); |
| continue; |
| } |
| |
| ri_memcpy(search_path_entry, start, n); |
| search_path_entry[n] = '\0'; |
| |
| char buf[PATH_MAX]; |
| char* d = NULL; |
| if (expand_origin) { |
| d = ri_strchr(search_path_entry, '$'); |
| } |
| if (d && (!ri_strncmp(d, "$ORIGIN", 7) || !ri_strncmp(d, "${ORIGIN}", 9))) { |
| if (!origin) { |
| get_origin(origin_buf, sizeof(origin_buf)); |
| origin = origin_buf; |
| } |
| |
| size_t s = 7; |
| if (d[1] == '{') { |
| s += 2; |
| } |
| ri_memcpy(buf, search_path_entry, d - search_path_entry); |
| buf[d - search_path_entry] = '\0'; |
| if (ri_strlen(buf) + ri_strlen(origin) + ri_strlen(d+s) >= sizeof(buf)) { |
| debug("path to loader %s%s%s too long", buf, origin, d+s); |
| continue; |
| } |
| |
| ri_strcat(buf, origin); |
| ri_strcat(buf, d+s); |
| } else { |
| ri_strcpy(buf, search_path_entry); |
| } |
| debug("trying loader %s at %s", loader_rel_path, buf); |
| if (!open_rel_loader(exe, buf, loader_rel_path, loader)) { |
| debug("opened loader %s at %s", loader_rel_path, buf); |
| return 0; |
| } |
| } |
| |
| return -1; |
| } |
| |
| static int find_and_open_loader(const ExeInfo* exe, const char* ld_library_path, OpenedLoader* loader) { |
| const char* loader_rel_path = LOADER_PATH; |
| |
| if (loader_rel_path[0] == '/') { |
| return open_loader(exe, loader_rel_path, loader); |
| } |
| |
| if (exe->secure) { |
| fatal("relinterp not supported for secure executables"); |
| } |
| |
| if (!search_path_list_for_loader(exe, loader_rel_path, ld_library_path, "LD_LIBRARY_PATH", false, loader)) { |
| return 0; |
| } |
| |
| if (!exe->search_paths || ri_strlen(exe->search_paths) == 0) { |
| // If no DT_RUNPATH search relative to the exe. |
| char origin[PATH_MAX]; |
| get_origin(origin, sizeof(origin)); |
| return open_rel_loader(exe, origin, loader_rel_path, loader); |
| } |
| |
| if (!search_path_list_for_loader(exe, loader_rel_path, exe->search_paths, "rpath", true, loader)) { |
| return 0; |
| } |
| |
| fatal("unable to find loader %s in rpath %s", loader_rel_path, exe->search_paths); |
| } |
| |
| // Use a trick to determine whether the executable has been relocated yet. This variable points to |
| // a variable in libc. It will be NULL if and only if the program hasn't been linked yet. This |
| // should accommodate these situations: |
| // - The program was actually statically-linked instead. |
| // - Either a PIE or non-PIE dynamic executable. |
| // - Any situation where the loader calls the executable's _start: |
| // - In normal operation, the kernel calls the executable's _start, _start jumps to the loader's |
| // entry point, which jumps to _start again after linking it. |
| // - The executable actually has its PT_INTERP set after all. |
| // - The user runs the loader, passing it the path of the executable. |
| // This C file must always be compiled as PIC, or else the linker will use a COPY relocation and |
| // duplicate "environ" into the executable. |
| static bool is_exe_relocated(void) { |
| // Use the GOT to get the address of environ. |
| extern char** environ; |
| void* read_environ = optimizer_barrier(&environ); |
| debug("read_environ = %p", read_environ); |
| return read_environ != NULL; |
| } |
| |
| void _start_c(long* raw_args) { |
| const KernelArguments args = read_args(raw_args); |
| const char* ld_library_path = NULL; |
| |
| for (size_t i = 0; i < args.envp_count; ++i) { |
| if (!ri_strcmp(args.envp[i], "RELINTERP_DEBUG=1")) { |
| g_debug = true; |
| } |
| if (!ri_strncmp(args.envp[i], "LD_LIBRARY_PATH=", 16)) { |
| ld_library_path = args.envp[i] + 16; |
| } |
| } |
| if (args.argc >= 1) { |
| g_prog_name = args.argv[0]; |
| } |
| |
| if (is_exe_relocated()) { |
| debug("exe is already relocated, starting main executable"); |
| int argc = raw_args[0]; |
| char **argv = (void *)(raw_args+1); |
| __libc_start_main(main, argc, argv, _init, _fini, 0); |
| } |
| |
| debug("entering relinterp"); |
| |
| const ExeInfo exe = get_exe_info(&args); |
| g_page_size = exe.page_size; |
| |
| OpenedLoader loader; |
| if (find_and_open_loader(&exe, ld_library_path, &loader)) { |
| fatal("failed to open loader"); |
| } |
| off_t len = ri_lseek(loader.fd, 0, SEEK_END); |
| if (len == (off_t)-1) fatal("lseek on %s failed: %s", loader.path, ri_strerror(g_errno)); |
| |
| void* loader_data = ri_mmap(NULL, len, PROT_READ, MAP_PRIVATE, loader.fd, 0); |
| if (loader_data == (void*)MAP_FAILED) { |
| fatal("could not mmap %s: %s", loader.path, ri_strerror(g_errno)); |
| } |
| |
| LoadedInterp interp = load_interp(&loader, (ElfW(Ehdr)*)loader_data); |
| if (ri_munmap(loader_data, len) != 0) fatal("munmap failed: %s", ri_strerror(g_errno)); |
| |
| debug("original auxv:"); |
| dump_auxv(&args); |
| |
| // Create a virtual phdr table that includes PT_INTERP, for the benefit of loaders that read the |
| // executable PT_INTERP. |
| insert_pt_interp_into_phdr_table(&args, &exe, loader.path); |
| ri_close(loader.fd); |
| |
| // TODO: /proc/pid/auxv isn't updated with the new auxv vector. Is it possible to update it? |
| // XXX: If we try to update it, we'd use prctl(PR_SET_MM, PR_SET_MM_AUXV, &vec, size, 0) |
| // Maybe updating it would be useful as a way to communicate the loader's base to a debugger. |
| // e.g. lldb uses AT_BASE in the aux vector, but it caches the values at process startup, so |
| // it wouldn't currently notice a changed value. |
| |
| // The loader uses AT_BASE to locate itself, so search for the entry and update it. Even though |
| // its value is always zero, the kernel still includes the entry[0]. If this changes (or we want |
| // to make weaker assumptions about the kernel's behavior), then we can copy the kernel arguments |
| // onto the stack (e.g. using alloca) before jumping to the loader's entry point. |
| // [0] https://github.com/torvalds/linux/blob/v5.13/fs/binfmt_elf.c#L263 |
| for (size_t i = 0; i < args.auxv_count; ++i) { |
| if (args.auxv[i].key == AT_BASE) { |
| args.auxv[i].value = (unsigned long)interp.base_addr; |
| debug("new auxv:"); |
| dump_auxv(&args); |
| debug("transferring to real loader"); |
| CRTJMP(interp.entry, raw_args); |
| } |
| } |
| fatal("AT_BASE not found in aux vector"); |
| } |
| |
| |
| // Normally gdb and lldb look for a symbol named "_dl_debug_state" in the |
| // interpreter to get notified when the dynamic loader has modified the |
| // list of shared libraries. When using relinterp, the debugger is not |
| // aware of the interpreter (PT_INTERP is unset and auxv AT_BASE is 0) so it |
| // doesn't know where to look for the symbol. It falls back to looking in the |
| // executable, so provide a symbol for it to find. The dynamic loader will |
| // need to forward its calls to its own _dl_debug_state symbol to this one. |
| // |
| // This has to be defined in a .c file because lldb looks for a symbol with |
| // DWARF language type DW_LANG_C. |
| extern void _dl_debug_state() { |
| } |