mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-13 03:51:10 +00:00
1028 lines
44 KiB
C
1028 lines
44 KiB
C
// Part of libddnatv-hook.so, included by ddnatv-hook.c (one translation unit, so the
|
|
// parts share their static functions and are built with the same command as before).
|
|
//
|
|
// What execve() and posix_spawn() need to know about a program: its ld.so, whether it is a
|
|
// dynamic ELF, a script and its interpreter, and the argument lists built from that.
|
|
|
|
// Relative exec paths ("./configure", "bin/tool", "../x.sh") must go through the same
|
|
// DDNATV_ROOT routing as absolute ones. Without this they were mistaken for Android host
|
|
// binaries and handed to the kernel unchanged, which then resolved "#!/bin/bash" (or a glibc
|
|
// PT_INTERP) literally and failed with "bad interpreter: No such file or directory".
|
|
// getcwd() is not virtualised, so cwd + path is a real path that starts with DDNATV_ROOT.
|
|
static const char *absolutize_exec_path(const char *path, char *buffer, size_t bufsize) {
|
|
if (!path || path[0] == '\0' || path[0] == '/') return path;
|
|
char cwd[PATH_MAX];
|
|
static char *(*real_getcwd)(char *, size_t) = NULL;
|
|
if (!real_getcwd) real_getcwd = (char *(*)(char *, size_t))dlsym(RTLD_NEXT, "getcwd");
|
|
if (!real_getcwd || !real_getcwd(cwd, sizeof(cwd))) return path;
|
|
|
|
while (path[0] == '.' && path[1] == '/') {
|
|
path += 2;
|
|
while (path[0] == '/') path++;
|
|
}
|
|
if (path[0] == '\0' || strcmp(path, ".") == 0) return path;
|
|
|
|
size_t cwd_len = strlen(cwd);
|
|
while (cwd_len > 1 && cwd[cwd_len - 1] == '/') cwd[--cwd_len] = '\0';
|
|
int n = snprintf(buffer, bufsize, "%s%s%s", cwd, (cwd_len == 1 && cwd[0] == '/') ? "" : "/", path);
|
|
if (n < 0 || (size_t)n >= bufsize) return path;
|
|
return buffer;
|
|
}
|
|
|
|
static char s_cached_ld_so[PATH_MAX] = {0};
|
|
static char s_cached_ld_root[PATH_MAX] = {0};
|
|
static uint16_t s_cached_e_machine = 0;
|
|
|
|
// ld.so inside `ddnatv_root` for an ELF with machine type `e_machine` (0 when unknown).
|
|
static int find_dynamic_linker_for_machine(const char *ddnatv_root, uint16_t e_machine, char *out_ld_so, size_t max_len) {
|
|
if (!ddnatv_root || ddnatv_root[0] == '\0' || !out_ld_so || max_len == 0) return 0;
|
|
|
|
if (s_cached_ld_so[0] != '\0' && strcmp(s_cached_ld_root, ddnatv_root) == 0 &&
|
|
e_machine != 0 && e_machine == s_cached_e_machine) {
|
|
ddnatv_copy_cstr(out_ld_so, max_len, s_cached_ld_so);
|
|
return 1;
|
|
}
|
|
|
|
const char *aarch64_cands[] = {
|
|
"/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1",
|
|
"/usr/lib/aarch64-linux-gnu/ld-2.39.so",
|
|
"/lib/ld-linux-aarch64.so.1",
|
|
"/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1",
|
|
"/usr/lib/ld-linux-aarch64.so.1",
|
|
"/usr/lib64/ld-linux-aarch64.so.1",
|
|
"/lib64/ld-linux-aarch64.so.1",
|
|
"/lib/aarch64-linux-gnu/ld-2.39.so",
|
|
NULL
|
|
};
|
|
|
|
const char *armhf_cands[] = {
|
|
"/usr/lib/arm-linux-gnueabihf/ld-linux-armhf.so.3",
|
|
"/usr/lib/arm-linux-gnueabihf/ld-2.39.so",
|
|
"/lib/ld-linux-armhf.so.3",
|
|
"/lib/arm-linux-gnueabihf/ld-linux-armhf.so.3",
|
|
"/usr/lib/ld-linux-armhf.so.3",
|
|
"/lib/arm-linux-gnueabihf/ld-2.39.so",
|
|
"/usr/lib/arm-linux-gnueabi/ld-linux.so.3",
|
|
"/lib/arm-linux-gnueabi/ld-linux.so.3",
|
|
NULL
|
|
};
|
|
|
|
const char *x86_64_cands[] = {
|
|
"/lib64/ld-linux-x86-64.so.2",
|
|
"/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
|
|
"/usr/lib/x86_64-linux-gnu/ld-2.39.so",
|
|
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",
|
|
NULL
|
|
};
|
|
|
|
const char **primary = NULL;
|
|
const char **secondary = NULL;
|
|
|
|
if (e_machine == 183) { // EM_AARCH64
|
|
primary = aarch64_cands;
|
|
secondary = NULL;
|
|
} else if (e_machine == 40) { // EM_ARM
|
|
primary = armhf_cands;
|
|
secondary = NULL;
|
|
} else if (e_machine == 62) { // EM_X86_64
|
|
primary = x86_64_cands;
|
|
secondary = NULL;
|
|
} else if (e_machine == 3) { // EM_386
|
|
primary = NULL;
|
|
secondary = NULL;
|
|
} else {
|
|
#if defined(__aarch64__)
|
|
primary = aarch64_cands;
|
|
secondary = armhf_cands;
|
|
#elif defined(__arm__)
|
|
primary = armhf_cands;
|
|
secondary = aarch64_cands;
|
|
#else
|
|
primary = x86_64_cands;
|
|
#endif
|
|
}
|
|
|
|
if (primary) {
|
|
for (int i = 0; primary[i] != NULL; i++) {
|
|
snprintf(out_ld_so, max_len, "%s%s", ddnatv_root, primary[i]);
|
|
if (access(out_ld_so, F_OK) == 0) {
|
|
if (e_machine != 0) {
|
|
ddnatv_copy_cstr(s_cached_ld_so, sizeof(s_cached_ld_so), out_ld_so);
|
|
ddnatv_copy_cstr(s_cached_ld_root, sizeof(s_cached_ld_root), ddnatv_root);
|
|
s_cached_e_machine = e_machine;
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
if (secondary) {
|
|
for (int i = 0; secondary[i] != NULL; i++) {
|
|
snprintf(out_ld_so, max_len, "%s%s", ddnatv_root, secondary[i]);
|
|
if (access(out_ld_so, F_OK) == 0) {
|
|
if (e_machine != 0) {
|
|
ddnatv_copy_cstr(s_cached_ld_so, sizeof(s_cached_ld_so), out_ld_so);
|
|
ddnatv_copy_cstr(s_cached_ld_root, sizeof(s_cached_ld_root), ddnatv_root);
|
|
s_cached_e_machine = e_machine;
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef DDNATV_HOST_TEST
|
|
// Test entry point taking a program instead of its machine type (ddnatv-pty.c, which the tests
|
|
// include too, has a find_dynamic_linker of its own).
|
|
static int hook_find_dynamic_linker(const char *ddnatv_root, const char *cmd, char *out_ld_so, size_t max_len) {
|
|
if (!ddnatv_root || ddnatv_root[0] == '\0' || !out_ld_so || max_len == 0) return 0;
|
|
|
|
uint16_t e_machine = 0;
|
|
if (cmd && cmd[0] != '\0') {
|
|
int fd = open(cmd, O_RDONLY);
|
|
if (fd >= 0) {
|
|
unsigned char ehdr[20];
|
|
ssize_t n = read(fd, ehdr, sizeof(ehdr));
|
|
close(fd);
|
|
if (n >= 20 && ehdr[0] == 0x7f && ehdr[1] == 'E' && ehdr[2] == 'L' && ehdr[3] == 'F') {
|
|
e_machine = (uint16_t)(ehdr[18] | (ehdr[19] << 8));
|
|
}
|
|
}
|
|
}
|
|
return find_dynamic_linker_for_machine(ddnatv_root, e_machine, out_ld_so, max_len);
|
|
}
|
|
#endif
|
|
|
|
static int check_elf_dynamic(int fd, const unsigned char *ehdr, ssize_t n) {
|
|
if (n < 52 || ehdr[0] != 0x7f || ehdr[1] != 'E' || ehdr[2] != 'L' || ehdr[3] != 'F') {
|
|
return 0;
|
|
}
|
|
if ((ehdr[4] != 1 && ehdr[4] != 2) || ehdr[5] != 1 /* ELFDATA2LSB */) {
|
|
return 0;
|
|
}
|
|
int is_64 = (ehdr[4] == 2);
|
|
uint64_t phoff = 0;
|
|
uint16_t phentsize = 0;
|
|
uint16_t phnum = 0;
|
|
if (is_64) {
|
|
if (n < 64) return 0;
|
|
memcpy(&phoff, ehdr + 32, sizeof(uint64_t));
|
|
memcpy(&phentsize, ehdr + 54, sizeof(uint16_t));
|
|
memcpy(&phnum, ehdr + 56, sizeof(uint16_t));
|
|
} else {
|
|
uint32_t phoff32 = 0;
|
|
memcpy(&phoff32, ehdr + 28, sizeof(uint32_t));
|
|
phoff = phoff32;
|
|
memcpy(&phentsize, ehdr + 42, sizeof(uint16_t));
|
|
memcpy(&phnum, ehdr + 44, sizeof(uint16_t));
|
|
}
|
|
uint16_t min_phentsize = is_64 ? 56 : 32;
|
|
if (phoff == 0 || phentsize < min_phentsize || phentsize > 4096 || phnum == 0) {
|
|
return 0;
|
|
}
|
|
if ((off_t)phoff < 0 || (uint64_t)(off_t)phoff != phoff) {
|
|
return 0;
|
|
}
|
|
if (lseek(fd, (off_t)phoff, SEEK_SET) < 0) {
|
|
return 0;
|
|
}
|
|
for (int i = 0; i < phnum && i < 128; i++) {
|
|
uint32_t p_type = 0;
|
|
if (read(fd, &p_type, sizeof(p_type)) != sizeof(p_type)) break;
|
|
if (p_type == 3 /* PT_INTERP */) {
|
|
return 1;
|
|
}
|
|
if (lseek(fd, (off_t)(phentsize - sizeof(p_type)), SEEK_CUR) < 0) break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// check_elf_dynamic() on the bytes already read: 1 or 0, or -1 when a program header lies past
|
|
// the buffer and the file has to be read after all. Program headers normally follow the ELF
|
|
// header directly, so this saves the lseek()/read() pairs.
|
|
static int elf_interp_in_buffer(const unsigned char *ehdr, ssize_t n) {
|
|
if (n < 52 || ehdr[0] != 0x7f || ehdr[1] != 'E' || ehdr[2] != 'L' || ehdr[3] != 'F') {
|
|
return 0;
|
|
}
|
|
if ((ehdr[4] != 1 && ehdr[4] != 2) || ehdr[5] != 1 /* ELFDATA2LSB */) {
|
|
return 0;
|
|
}
|
|
int is_64 = (ehdr[4] == 2);
|
|
uint64_t phoff = 0;
|
|
uint16_t phentsize = 0;
|
|
uint16_t phnum = 0;
|
|
if (is_64) {
|
|
if (n < 64) return 0;
|
|
memcpy(&phoff, ehdr + 32, sizeof(uint64_t));
|
|
memcpy(&phentsize, ehdr + 54, sizeof(uint16_t));
|
|
memcpy(&phnum, ehdr + 56, sizeof(uint16_t));
|
|
} else {
|
|
uint32_t phoff32 = 0;
|
|
memcpy(&phoff32, ehdr + 28, sizeof(uint32_t));
|
|
phoff = phoff32;
|
|
memcpy(&phentsize, ehdr + 42, sizeof(uint16_t));
|
|
memcpy(&phnum, ehdr + 44, sizeof(uint16_t));
|
|
}
|
|
uint16_t min_phentsize = is_64 ? 56 : 32;
|
|
if (phoff == 0 || phentsize < min_phentsize || phentsize > 4096 || phnum == 0) {
|
|
return 0;
|
|
}
|
|
if ((off_t)phoff < 0 || (uint64_t)(off_t)phoff != phoff) {
|
|
return 0;
|
|
}
|
|
for (int i = 0; i < phnum && i < 128; i++) {
|
|
uint64_t off = phoff + (uint64_t)i * phentsize;
|
|
if (off > (uint64_t)n || (uint64_t)n - off < sizeof(uint32_t)) return -1;
|
|
uint32_t p_type = 0;
|
|
memcpy(&p_type, ehdr + off, sizeof(p_type));
|
|
if (p_type == 3 /* PT_INTERP */) {
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Whether the interpreter of a 64-bit ELF is Android's own linker (/system/bin/linker64). A Go
|
|
// program built with GOOS=android is a PIE that asks for it, so it has a PT_INTERP and looks like a
|
|
// dynamic program, but it has no C library and makes its own system calls like every Go program
|
|
// (#42). `h` holds the `n` first bytes of the file; the program headers usually are among them.
|
|
static int elf_interp_is_android(int fd, const unsigned char *h, ssize_t n) {
|
|
if (n < 64 || h[4] != 2) return 0;
|
|
uint64_t phoff = 0;
|
|
uint16_t phentsize = 0, phnum = 0;
|
|
memcpy(&phoff, h + 32, sizeof(phoff));
|
|
memcpy(&phentsize, h + 54, sizeof(phentsize));
|
|
memcpy(&phnum, h + 56, sizeof(phnum));
|
|
if (phoff == 0 || phoff > (1ULL << 40) || phentsize < 56 || phentsize > 4096 || phnum == 0) return 0;
|
|
for (int i = 0; i < phnum && i < 128; i++) {
|
|
unsigned char e[56];
|
|
uint64_t off = phoff + (uint64_t)i * phentsize;
|
|
if (off + sizeof(e) <= (uint64_t)n) {
|
|
memcpy(e, h + off, sizeof(e));
|
|
} else if (pread(fd, e, sizeof(e), (off_t)off) != (ssize_t)sizeof(e)) {
|
|
return 0;
|
|
}
|
|
uint32_t p_type = 0;
|
|
memcpy(&p_type, e, sizeof(p_type));
|
|
if (p_type != 3 /* PT_INTERP */) continue;
|
|
uint64_t p_offset = 0, p_filesz = 0;
|
|
memcpy(&p_offset, e + 8, sizeof(p_offset));
|
|
memcpy(&p_filesz, e + 32, sizeof(p_filesz));
|
|
char interp[48];
|
|
size_t want = p_filesz < sizeof(interp) - 1 ? (size_t)p_filesz : sizeof(interp) - 1;
|
|
if (want == 0 || p_offset > (1ULL << 40)) return 0;
|
|
ssize_t got = pread(fd, interp, want, (off_t)p_offset);
|
|
if (got <= 0) return 0;
|
|
interp[got] = '\0';
|
|
return strncmp(interp, "/system/bin/linker", 18) == 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The kernel follows at most four nested "#!" interpreters, then fails with ELOOP.
|
|
#define DDNATV_MAX_SHEBANG_DEPTH 4
|
|
|
|
// What execve()/posix_spawn() need to know about the file they are about to start, from a
|
|
// single open()/read() (they used to open it up to three times).
|
|
struct exec_probe {
|
|
char hdr[256]; // first bytes of the file, NUL-terminated for "#!" parsing
|
|
ssize_t n; // bytes read; -1 when the file could not be opened
|
|
int is_elf;
|
|
int is_dynamic; // ELF with PT_INTERP: has to be started through ld.so
|
|
int android_interp; // AArch64 ELF whose PT_INTERP is Android's linker (a Go program for GOOS=android)
|
|
uint16_t e_machine;
|
|
mode_t mode; // st_mode, filled in for dynamic ELF files only
|
|
int runpath_is_rpath; // runpath holds a DT_RPATH (searched before LD_LIBRARY_PATH), not a DT_RUNPATH
|
|
char runpath[1024]; // DT_RUNPATH (else DT_RPATH) of a dynamic ELF as written in it, or ""
|
|
};
|
|
|
|
// The DT_RUNPATH of a dynamic ELF (its DT_RPATH when it has none, as ld.so does) into `out`, as
|
|
// written in the file. ld.so looks for the program's libraries there, but it opens those
|
|
// directories itself, without the hook: an absolute one such as /usr/lib/aarch64-linux-gnu/android
|
|
// (Ubuntu's adb and fastboot keep their own libbase, liblog, libssl... there) is looked for in
|
|
// Android's tree and the program does not start. The exec adds the rootfs copies to the
|
|
// --library-path it gives ld.so (build_ld_library_path). Returns 1 for a DT_RUNPATH, 2 for a
|
|
// DT_RPATH, 0 when there is neither. The headers are read in pieces, so that their number does
|
|
// not matter; a list longer than `out` keeps its first whole entries (ld.so still has the rest).
|
|
static int elf_read_dynamic(int fd, const unsigned char *h, ssize_t n, char *out, size_t outsz,
|
|
char *needed, size_t needed_size, int *needed_count);
|
|
|
|
static int elf_read_runpath(int fd, const unsigned char *h, ssize_t n, char *out, size_t outsz) {
|
|
return elf_read_dynamic(fd, h, n, out, outsz, NULL, 0, NULL);
|
|
}
|
|
|
|
// elf_read_runpath(), and with `needed` the DT_NEEDED names too: one after the other, each ended by a
|
|
// NUL, their number in *needed_count (as many as fit).
|
|
#define ELF_MAX_NEEDED 128
|
|
static int elf_read_dynamic(int fd, const unsigned char *h, ssize_t n, char *out, size_t outsz,
|
|
char *needed, size_t needed_size, int *needed_count) {
|
|
if (needed_count) *needed_count = 0;
|
|
if (!out || outsz < 2) return 0;
|
|
out[0] = '\0';
|
|
if (n < 52 || h[0] != 0x7f || h[1] != 'E' || h[2] != 'L' || h[3] != 'F' || h[5] != 1 /* LSB */) return 0;
|
|
int is_64 = (h[4] == 2);
|
|
if (!is_64 && h[4] != 1) return 0;
|
|
if (is_64 && n < 64) return 0;
|
|
uint64_t phoff = 0;
|
|
uint16_t phentsize = 0, phnum = 0;
|
|
if (is_64) {
|
|
memcpy(&phoff, h + 32, 8);
|
|
memcpy(&phentsize, h + 54, 2);
|
|
memcpy(&phnum, h + 56, 2);
|
|
} else {
|
|
uint32_t off32 = 0;
|
|
memcpy(&off32, h + 28, 4);
|
|
phoff = off32;
|
|
memcpy(&phentsize, h + 42, 2);
|
|
memcpy(&phnum, h + 44, 2);
|
|
}
|
|
size_t ph_size = is_64 ? 56 : 32;
|
|
if (phoff == 0 || phoff > (1ULL << 40) || phentsize < ph_size || phentsize > 1024 || phnum == 0) return 0;
|
|
|
|
// PT_DYNAMIC, and the PT_LOAD segments that map an address of the file to its offset
|
|
unsigned char buf[2048];
|
|
uint64_t dyn_off = 0, dyn_size = 0;
|
|
uint64_t load_vaddr[16], load_off[16], load_size[16];
|
|
int loads = 0;
|
|
size_t per_read = sizeof(buf) / phentsize;
|
|
for (size_t first = 0; first < phnum; first += per_read) {
|
|
size_t count = phnum - first < per_read ? phnum - first : per_read;
|
|
size_t bytes = count * phentsize;
|
|
if (pread(fd, buf, bytes, (off_t)(phoff + first * phentsize)) != (ssize_t)bytes) return 0;
|
|
for (size_t i = 0; i < count; i++) {
|
|
const unsigned char *e = buf + i * phentsize;
|
|
uint32_t type = 0;
|
|
memcpy(&type, e, 4);
|
|
uint64_t off = 0, vaddr = 0, filesz = 0;
|
|
if (is_64) {
|
|
memcpy(&off, e + 8, 8);
|
|
memcpy(&vaddr, e + 16, 8);
|
|
memcpy(&filesz, e + 32, 8);
|
|
} else {
|
|
uint32_t v = 0;
|
|
memcpy(&v, e + 4, 4); off = v;
|
|
memcpy(&v, e + 8, 4); vaddr = v;
|
|
memcpy(&v, e + 16, 4); filesz = v;
|
|
}
|
|
if (type == 2 /* PT_DYNAMIC */) {
|
|
dyn_off = off;
|
|
dyn_size = filesz;
|
|
} else if (type == 1 /* PT_LOAD */ && loads < 16) {
|
|
load_vaddr[loads] = vaddr;
|
|
load_off[loads] = off;
|
|
load_size[loads] = filesz;
|
|
loads++;
|
|
}
|
|
}
|
|
}
|
|
if (dyn_size == 0 || dyn_off > (1ULL << 40)) return 0;
|
|
if (dyn_size > (1u << 20)) dyn_size = 1u << 20;
|
|
|
|
size_t ent = is_64 ? 16 : 8;
|
|
uint64_t strtab = 0, runpath = 0, rpath = 0;
|
|
int has_strtab = 0, has_runpath = 0, has_rpath = 0, done = 0;
|
|
uint64_t needed_at[ELF_MAX_NEEDED];
|
|
int nneeded = 0;
|
|
for (uint64_t at = 0; at + ent <= dyn_size && !done;) {
|
|
size_t want = dyn_size - at < sizeof(buf) ? (size_t)(dyn_size - at) : sizeof(buf);
|
|
want -= want % ent;
|
|
ssize_t got = pread(fd, buf, want, (off_t)(dyn_off + at));
|
|
if (got < (ssize_t)ent) break;
|
|
for (size_t i = 0; i + ent <= (size_t)got; i += ent) {
|
|
int64_t tag = 0;
|
|
uint64_t val = 0;
|
|
if (is_64) {
|
|
memcpy(&tag, buf + i, 8);
|
|
memcpy(&val, buf + i + 8, 8);
|
|
} else {
|
|
int32_t tv = 0;
|
|
uint32_t vv = 0;
|
|
memcpy(&tv, buf + i, 4);
|
|
memcpy(&vv, buf + i + 4, 4);
|
|
tag = tv;
|
|
val = vv;
|
|
}
|
|
if (tag == 0 /* DT_NULL */) { done = 1; break; }
|
|
if (tag == 5 /* DT_STRTAB */) { strtab = val; has_strtab = 1; }
|
|
else if (tag == 29 /* DT_RUNPATH */) { runpath = val; has_runpath = 1; }
|
|
else if (tag == 15 /* DT_RPATH */) { rpath = val; has_rpath = 1; }
|
|
else if (tag == 1 /* DT_NEEDED */ && needed && nneeded < ELF_MAX_NEEDED) needed_at[nneeded++] = val;
|
|
}
|
|
at += (uint64_t)got - (uint64_t)got % ent;
|
|
}
|
|
if (!has_strtab || (!has_runpath && !has_rpath)) return 0;
|
|
uint64_t str_index = has_runpath ? runpath : rpath;
|
|
|
|
// DT_STRTAB is an address: the file offset is that of the PT_LOAD segment holding it
|
|
uint64_t strtab_off = 0;
|
|
int mapped = 0;
|
|
for (int i = 0; i < loads; i++) {
|
|
if (strtab >= load_vaddr[i] && strtab - load_vaddr[i] < load_size[i]) {
|
|
strtab_off = load_off[i] + (strtab - load_vaddr[i]);
|
|
mapped = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!mapped || str_index > (1ULL << 32)) return 0;
|
|
if (needed && needed_count && needed_size > 0) {
|
|
size_t used = 0;
|
|
for (int i = 0; i < nneeded; i++) {
|
|
char name[256];
|
|
ssize_t got = pread(fd, name, sizeof(name) - 1, (off_t)(strtab_off + needed_at[i]));
|
|
if (got <= 0) continue;
|
|
name[got] = '\0';
|
|
size_t len = strnlen(name, (size_t)got);
|
|
if (len == 0 || len == (size_t)got || used + len + 1 > needed_size) continue;
|
|
memcpy(needed + used, name, len + 1);
|
|
used += len + 1;
|
|
(*needed_count)++;
|
|
}
|
|
}
|
|
ssize_t len = pread(fd, out, outsz - 1, (off_t)(strtab_off + str_index));
|
|
if (len <= 0) {
|
|
out[0] = '\0';
|
|
return 0;
|
|
}
|
|
out[len] = '\0';
|
|
if (strnlen(out, (size_t)len) == (size_t)len) {
|
|
// Longer than `out`: its whole entries up to there
|
|
char *last = strrchr(out, ':');
|
|
if (!last) {
|
|
out[0] = '\0';
|
|
return 0;
|
|
}
|
|
*last = '\0';
|
|
}
|
|
if (out[0] == '\0') return 0;
|
|
return has_runpath ? 1 : 2;
|
|
}
|
|
|
|
// Appends dir[0..len) to the ':'-separated out_buf[0..used) unless it is there already.
|
|
static size_t append_lib_dir(char *out_buf, size_t out_size, size_t used, const char *dir, size_t len) {
|
|
if (len == 0 || lib_dir_list_has(out_buf, dir, len)) return used;
|
|
size_t need = (used > 0 ? 1 : 0) + len;
|
|
if (used + need >= out_size) return used;
|
|
if (used > 0) out_buf[used++] = ':';
|
|
memcpy(out_buf + used, dir, len);
|
|
used += len;
|
|
out_buf[used] = '\0';
|
|
return used;
|
|
}
|
|
|
|
// The directories of `runpath` (a DT_RUNPATH or DT_RPATH), in their order, appended to
|
|
// out_buf[0..used): the absolute ones that lie in the rootfs mapped into it, and those that start
|
|
// with $ORIGIN with it replaced by the directory of `exe` (the program's real path, in the rootfs
|
|
// already), so that both keep their places. ld.so opens them itself, without the hook, so
|
|
// /usr/lib/aarch64-linux-gnu/android would be looked for in Android's tree. Others ($LIB,
|
|
// $PLATFORM, directories outside the rootfs) are left to ld.so. Returns the new length.
|
|
static size_t append_runpath_dirs(char *out_buf, size_t out_size, size_t used, const char *runpath, const char *exe) {
|
|
size_t origin_len = 0;
|
|
if (exe) {
|
|
const char *slash = strrchr(exe, '/');
|
|
if (slash) origin_len = slash > exe ? (size_t)(slash - exe) : 1;
|
|
}
|
|
for (const char *p = runpath; p && *p;) {
|
|
const char *end = strchr(p, ':');
|
|
size_t len = end ? (size_t)(end - p) : strlen(p);
|
|
if (len > 0 && len < PATH_MAX) {
|
|
char tok[PATH_MAX];
|
|
memcpy(tok, p, len);
|
|
tok[len] = '\0';
|
|
size_t skip = 0;
|
|
if (strncmp(tok, "$ORIGIN", 7) == 0) skip = 7;
|
|
else if (strncmp(tok, "${ORIGIN}", 9) == 0) skip = 9;
|
|
if (skip && (tok[skip] == '\0' || tok[skip] == '/') && origin_len > 0 && !strchr(tok + skip, '$')) {
|
|
char full[PATH_MAX];
|
|
size_t rest = len - skip;
|
|
if (origin_len + rest < sizeof(full)) {
|
|
memcpy(full, exe, origin_len);
|
|
memcpy(full + origin_len, tok + skip, rest);
|
|
full[origin_len + rest] = '\0';
|
|
used = append_lib_dir(out_buf, out_size, used, full, origin_len + rest);
|
|
}
|
|
} else if (tok[0] == '/' && len > 1 && !strchr(tok, '$')) {
|
|
char rw_buf[PATH_MAX];
|
|
const char *rw = rewrite_path_in_root(tok, rw_buf, sizeof(rw_buf));
|
|
if (strcmp(rw, tok) != 0) used = append_lib_dir(out_buf, out_size, used, rw, strlen(rw));
|
|
}
|
|
}
|
|
if (!end) break;
|
|
p = end + 1;
|
|
}
|
|
return used;
|
|
}
|
|
|
|
// Whether dir[0..len) is one of the rootfs library directories The native userland adds itself (also what its
|
|
// shells export in LD_LIBRARY_PATH).
|
|
static int is_rootfs_default_lib_dir(const char *dir, size_t len) {
|
|
if (len <= g_ddnatv_root_len || memcmp(dir, g_ddnatv_root, g_ddnatv_root_len) != 0) return 0;
|
|
const char *rel = dir + g_ddnatv_root_len;
|
|
size_t rel_len = len - g_ddnatv_root_len;
|
|
for (size_t i = 0; i < sizeof(k_rootfs_lib_dirs) / sizeof(k_rootfs_lib_dirs[0]); i++) {
|
|
if (strlen(k_rootfs_lib_dirs[i]) == rel_len && memcmp(k_rootfs_lib_dirs[i], rel, rel_len) == 0) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Where the program is among the arguments of ld.so started directly (ldd runs "ld.so PROGRAM" with
|
|
// LD_TRACE_LOADED_OBJECTS set; "ld.so --list PROGRAM" and "ld.so --verify PROGRAM" are the same): its
|
|
// index, or -1 when there is none; *has_library_path says whether the caller gives --library-path itself.
|
|
static int ldso_program_index(char *const argv[], int *has_library_path) {
|
|
if (has_library_path) *has_library_path = 0;
|
|
if (!argv || !argv[0]) return -1;
|
|
static const char *const with_value[] = { "--library-path", "--preload", "--argv0", "--inhibit-rpath", "--audit",
|
|
"--glibc-hwcaps-prepend", "--glibc-hwcaps-mask" };
|
|
for (int i = 1; argv[i]; i++) {
|
|
const char *a = argv[i];
|
|
if (strcmp(a, "--library-path") == 0 && has_library_path) *has_library_path = 1;
|
|
if (a[0] != '-' || a[1] != '-') return i;
|
|
if (a[2] == '\0') return argv[i + 1] ? i + 1 : -1;
|
|
int takes_value = 0;
|
|
for (size_t k = 0; k < sizeof(with_value) / sizeof(with_value[0]); k++) {
|
|
if (strcmp(a, with_value[k]) == 0) takes_value = 1;
|
|
}
|
|
if (takes_value) {
|
|
if (!argv[i + 1]) return -1;
|
|
i++;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// ---- dlopen() of a library with a RUNPATH ---------------------------------------------------
|
|
// A library that a program loads with dlopen() looks for its own dependencies in its RUNPATH, which ld.so
|
|
// opens without the hook: PulseAudio's modules (RUNPATH /usr/lib/aarch64-linux-gnu/pulseaudio) and many
|
|
// plugins did not load. Before the real dlopen(), the dependencies found in the rootfs copies of those
|
|
// directories are loaded by their full path, depth first; ld.so then finds them loaded, by their name.
|
|
#define DLOPEN_PRELOAD_DEPTH 6
|
|
|
|
static int dlopen_preload_deps(const char *path, void *(*orig_dlopen)(const char *, int), int depth) {
|
|
if (depth > DLOPEN_PRELOAD_DEPTH || !path || path[0] != '/') return 0;
|
|
int fd = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return 0;
|
|
unsigned char hdr[64];
|
|
ssize_t n = pread(fd, hdr, sizeof(hdr), 0);
|
|
char runpath[1024];
|
|
char needed[4096];
|
|
int count = 0;
|
|
int kind = n > 0 ? elf_read_dynamic(fd, hdr, n, runpath, sizeof(runpath), needed, sizeof(needed), &count) : 0;
|
|
syscall(SYS_close, fd);
|
|
if (!kind || count == 0) return 0;
|
|
// the RUNPATH's directories, mapped into the rootfs ($ORIGIN from this library's own place)
|
|
char dirs[PATH_MAX * 2];
|
|
dirs[0] = '\0';
|
|
size_t used = append_runpath_dirs(dirs, sizeof(dirs), 0, runpath, path);
|
|
if (used == 0) return 0;
|
|
int loaded = 0;
|
|
const char *name = needed;
|
|
for (int i = 0; i < count; i++, name += strlen(name) + 1) {
|
|
if (strchr(name, '/')) continue;
|
|
void *already = orig_dlopen(name, RTLD_LAZY | RTLD_NOLOAD);
|
|
if (already) {
|
|
dlclose(already);
|
|
continue;
|
|
}
|
|
for (const char *p = dirs; *p;) {
|
|
const char *end = strchr(p, ':');
|
|
size_t len = end ? (size_t)(end - p) : strlen(p);
|
|
char candidate[PATH_MAX];
|
|
if (len + 1 + strlen(name) < sizeof(candidate)) {
|
|
memcpy(candidate, p, len);
|
|
candidate[len] = '/';
|
|
strcpy(candidate + len + 1, name);
|
|
int probe_fd = (int)syscall(SYS_openat, AT_FDCWD, candidate, O_PATH | O_CLOEXEC);
|
|
if (probe_fd >= 0) {
|
|
syscall(SYS_close, probe_fd);
|
|
dlopen_preload_deps(candidate, orig_dlopen, depth + 1);
|
|
if (orig_dlopen(candidate, RTLD_LAZY | RTLD_LOCAL)) loaded++;
|
|
break;
|
|
}
|
|
}
|
|
if (!end) break;
|
|
p = end + 1;
|
|
}
|
|
}
|
|
return loaded;
|
|
}
|
|
|
|
// ---- /etc/ld.so.conf -------------------------------------------------------------------------
|
|
// The library directories of /etc/ld.so.conf and the files it includes (/etc/ld.so.conf.d/*.conf),
|
|
// mapped into the rootfs. On Linux, ldconfig turns them into /etc/ld.so.cache, which ld.so reads.
|
|
// Here ld.so would read Android's /etc/ld.so.cache, and the paths in the cache are not mapped into
|
|
// the rootfs either, so the exec hands the directories to ld.so in --library-path instead. They are
|
|
// read again only when /etc/ld.so.conf, /etc/ld.so.conf.d or /etc/ld.so.cache changes: a .conf file
|
|
// added or removed counts at once, an edit of one after ldconfig, as on Linux.
|
|
#define LDCONF_MAX_DEPTH 4
|
|
#define LDCONF_MAX_INCLUDES 64
|
|
|
|
struct ldconf_sig {
|
|
int64_t t[6]; // mtimes (seconds, nanoseconds) of ld.so.conf, ld.so.conf.d and ld.so.cache; 0 if missing
|
|
};
|
|
|
|
static struct {
|
|
int valid;
|
|
struct ldconf_sig sig;
|
|
char root[PATH_MAX];
|
|
char dirs[PATH_MAX * 2];
|
|
} g_ldconf;
|
|
static pthread_mutex_t g_ldconf_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
// fstatat() behind the hook's stat emulation (which would map the hard link table in a child that is
|
|
// about to exec): only the times are wanted.
|
|
static int ldconf_stat_time(const char *real, int64_t *sec, int64_t *nsec) {
|
|
static int (*f)(int, const char *, struct stat64 *, int) = NULL;
|
|
int (*fn)(int, const char *, struct stat64 *, int) = __atomic_load_n(&f, __ATOMIC_RELAXED);
|
|
if (!fn) {
|
|
fn = (int (*)(int, const char *, struct stat64 *, int))dlsym(RTLD_NEXT, "fstatat64");
|
|
if (!fn) return -1;
|
|
__atomic_store_n(&f, fn, __ATOMIC_RELAXED);
|
|
}
|
|
struct stat64 st;
|
|
if (fn(AT_FDCWD, real, &st, 0) != 0) return -1;
|
|
*sec = (int64_t)st.st_mtim.tv_sec;
|
|
*nsec = (int64_t)st.st_mtim.tv_nsec;
|
|
return 0;
|
|
}
|
|
|
|
static void ldconf_signature(struct ldconf_sig *sig) {
|
|
static const char *const files[3] = { "/etc/ld.so.conf", "/etc/ld.so.conf.d", "/etc/ld.so.cache" };
|
|
for (int i = 0; i < 3; i++) {
|
|
char path[PATH_MAX];
|
|
size_t n = ddnatv_append(path, sizeof(path), 0, g_ddnatv_root, g_ddnatv_root_len);
|
|
ddnatv_append(path, sizeof(path), n, files[i], strlen(files[i]));
|
|
if (ldconf_stat_time(path, &sig->t[i * 2], &sig->t[i * 2 + 1]) != 0) {
|
|
sig->t[i * 2] = 0;
|
|
sig->t[i * 2 + 1] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The whole of the rootfs file `logical` (an absolute path inside Ubuntu) into buf, NUL-terminated.
|
|
static ssize_t ldconf_read(const char *logical, char *buf, size_t bufsz) {
|
|
char rw_buf[PATH_MAX];
|
|
const char *real = rewrite_path_in_root(logical, rw_buf, sizeof(rw_buf));
|
|
int fd = (int)syscall(SYS_openat, AT_FDCWD, real, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return -1;
|
|
size_t got = 0;
|
|
while (got + 1 < bufsz) {
|
|
ssize_t n = (ssize_t)syscall(SYS_read, fd, buf + got, bufsz - 1 - got);
|
|
if (n <= 0) break;
|
|
got += (size_t)n;
|
|
}
|
|
syscall(SYS_close, fd);
|
|
buf[got] = '\0';
|
|
return (ssize_t)got;
|
|
}
|
|
|
|
static int ldconf_name_cmp(const void *a, const void *b) {
|
|
return strcmp(*(const char *const *)a, *(const char *const *)b);
|
|
}
|
|
|
|
static void ldconf_parse_file(const char *logical, char *out, size_t outsz, size_t *used, int depth, int *includes);
|
|
|
|
// include PATTERN: the files of one directory that match a shell pattern, in name order (as glob()).
|
|
// Patterns in the directory part are not supported (Ubuntu uses /etc/ld.so.conf.d/*.conf).
|
|
static void ldconf_include(const char *pattern, char *out, size_t outsz, size_t *used, int depth, int *includes) {
|
|
const char *slash = strrchr(pattern, '/');
|
|
if (!slash || slash == pattern) return;
|
|
size_t dir_len = (size_t)(slash - pattern);
|
|
if (dir_len >= PATH_MAX || memchr(pattern, '*', dir_len) || memchr(pattern, '?', dir_len) || memchr(pattern, '[', dir_len)) return;
|
|
char dir[PATH_MAX];
|
|
memcpy(dir, pattern, dir_len);
|
|
dir[dir_len] = '\0';
|
|
const char *name_pattern = slash + 1;
|
|
if (!strpbrk(name_pattern, "*?[")) {
|
|
ldconf_parse_file(pattern, out, outsz, used, depth + 1, includes);
|
|
return;
|
|
}
|
|
char rw_buf[PATH_MAX];
|
|
const char *real = rewrite_path_in_root(dir, rw_buf, sizeof(rw_buf));
|
|
int fd = (int)syscall(SYS_openat, AT_FDCWD, real, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
if (fd < 0) return;
|
|
char names_buf[4096];
|
|
char *names[LDCONF_MAX_INCLUDES];
|
|
int count = 0;
|
|
size_t names_used = 0;
|
|
char dents[4096];
|
|
for (;;) {
|
|
long n = syscall(SYS_getdents64, fd, dents, sizeof(dents));
|
|
if (n <= 0) break;
|
|
for (long at = 0; at < n;) {
|
|
unsigned short reclen;
|
|
memcpy(&reclen, dents + at + 16, sizeof(reclen));
|
|
const char *name = dents + at + 19;
|
|
if (reclen == 0) break;
|
|
size_t len = strnlen(name, (size_t)reclen - 19);
|
|
if (count < LDCONF_MAX_INCLUDES && names_used + len + 1 <= sizeof(names_buf) &&
|
|
fnmatch(name_pattern, name, FNM_PERIOD) == 0) {
|
|
memcpy(names_buf + names_used, name, len + 1);
|
|
names[count++] = names_buf + names_used;
|
|
names_used += len + 1;
|
|
}
|
|
at += reclen;
|
|
}
|
|
}
|
|
syscall(SYS_close, fd);
|
|
qsort(names, (size_t)count, sizeof(names[0]), ldconf_name_cmp);
|
|
for (int i = 0; i < count; i++) {
|
|
char file[PATH_MAX];
|
|
if (dir_len + 1 + strlen(names[i]) >= sizeof(file)) continue;
|
|
snprintf(file, sizeof(file), "%s/%s", dir, names[i]);
|
|
ldconf_parse_file(file, out, outsz, used, depth + 1, includes);
|
|
}
|
|
}
|
|
|
|
// One ld.so.conf-style file: a directory per line, "include PATTERN..." (relative to the file's own
|
|
// directory), "#" comments, and the old "hwcap" lines, which are skipped as glibc does.
|
|
static void ldconf_parse_file(const char *logical, char *out, size_t outsz, size_t *used, int depth, int *includes) {
|
|
if (depth > LDCONF_MAX_DEPTH || ++*includes > LDCONF_MAX_INCLUDES) return;
|
|
char text[8192];
|
|
if (ldconf_read(logical, text, sizeof(text)) < 0) return;
|
|
for (char *line = text; line && *line;) {
|
|
char *next = strchr(line, '\n');
|
|
if (next) *next++ = '\0';
|
|
char *hash = strchr(line, '#');
|
|
if (hash) *hash = '\0';
|
|
while (*line == ' ' || *line == '\t' || *line == '\r') line++;
|
|
size_t len = strlen(line);
|
|
while (len > 0 && (line[len - 1] == ' ' || line[len - 1] == '\t' || line[len - 1] == '\r')) line[--len] = '\0';
|
|
if (len == 0) {
|
|
line = next;
|
|
continue;
|
|
}
|
|
if (strncmp(line, "include", 7) == 0 && (line[7] == ' ' || line[7] == '\t')) {
|
|
for (char *p = line + 8; *p;) {
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
if (!*p) break;
|
|
char *end = p;
|
|
while (*end && *end != ' ' && *end != '\t') end++;
|
|
char saved = *end;
|
|
*end = '\0';
|
|
char pattern[PATH_MAX];
|
|
if (p[0] == '/') {
|
|
snprintf(pattern, sizeof(pattern), "%s", p);
|
|
} else {
|
|
const char *slash = strrchr(logical, '/');
|
|
int base = slash ? (int)(slash - logical) : 0;
|
|
snprintf(pattern, sizeof(pattern), "%.*s/%s", base, logical, p);
|
|
}
|
|
ldconf_include(pattern, out, outsz, used, depth, includes);
|
|
*end = saved;
|
|
p = end;
|
|
}
|
|
} else if (strncmp(line, "hwcap", 5) == 0 && (line[5] == ' ' || line[5] == '\t')) {
|
|
// ignored by ldconfig since glibc 2.37
|
|
} else if (line[0] == '/') {
|
|
// "DIR" or the old "DIR=TYPE" / "DIR TYPE"
|
|
size_t dlen = strcspn(line, "= \t");
|
|
while (dlen > 1 && line[dlen - 1] == '/') dlen--;
|
|
if (dlen > 0 && dlen < PATH_MAX) {
|
|
char dir[PATH_MAX];
|
|
memcpy(dir, line, dlen);
|
|
dir[dlen] = '\0';
|
|
char rw_buf[PATH_MAX];
|
|
const char *rw = rewrite_path_in_root(dir, rw_buf, sizeof(rw_buf));
|
|
if (strcmp(rw, dir) != 0) *used = append_lib_dir(out, outsz, *used, rw, strlen(rw));
|
|
}
|
|
}
|
|
line = next;
|
|
}
|
|
}
|
|
|
|
static void ldconf_parse(char *out, size_t outsz) {
|
|
out[0] = '\0';
|
|
size_t used = 0;
|
|
int includes = 0;
|
|
ldconf_parse_file("/etc/ld.so.conf", out, outsz, &used, 0, &includes);
|
|
}
|
|
|
|
// The ld.so.conf directories into out (':'-separated, "" when there are none). Uses the copy of an
|
|
// earlier call while the files have not changed: the parent finds it when it first forks, and its
|
|
// children only compare the three times. Never waits for the lock: a child forked while another
|
|
// thread held it reads the files itself.
|
|
static void ldconf_dirs(char *out, size_t outsz) {
|
|
if (!out || outsz == 0) return;
|
|
out[0] = '\0';
|
|
if (g_ddnatv_root[0] == '\0') return;
|
|
struct ldconf_sig sig;
|
|
ldconf_signature(&sig);
|
|
if (sig.t[0] == 0 && sig.t[1] == 0) return; // neither ld.so.conf nor ld.so.conf.d
|
|
if (pthread_mutex_trylock(&g_ldconf_lock) != 0) {
|
|
ldconf_parse(out, outsz);
|
|
return;
|
|
}
|
|
if (!g_ldconf.valid || memcmp(&g_ldconf.sig, &sig, sizeof(sig)) != 0 || strcmp(g_ldconf.root, g_ddnatv_root) != 0) {
|
|
ldconf_parse(g_ldconf.dirs, sizeof(g_ldconf.dirs));
|
|
g_ldconf.sig = sig;
|
|
snprintf(g_ldconf.root, sizeof(g_ldconf.root), "%s", g_ddnatv_root);
|
|
g_ldconf.valid = 1;
|
|
}
|
|
snprintf(out, outsz, "%s", g_ldconf.dirs);
|
|
pthread_mutex_unlock(&g_ldconf_lock);
|
|
}
|
|
|
|
// The --library-path ld.so is given for the rootfs program `exe`, in the order ld.so searches on
|
|
// Linux: the program's RPATH, the caller's LD_LIBRARY_PATH (the rootfs paths in it mapped), its
|
|
// RUNPATH, the directories of /etc/ld.so.conf (ld.so's cache), and the rootfs defaults last. The native userland's
|
|
// shells export the defaults in LD_LIBRARY_PATH too, where Linux has none; when there is anything to
|
|
// put before them they move to the end, so that a library the program keeps for itself wins over
|
|
// one of the same name in /usr/lib. Runs after the hook's setup, so the
|
|
// rewrites are those of rewrite_path().
|
|
static void build_ld_library_path(char *const envp[], const char *runpath, int is_rpath, const char *exe,
|
|
char *out_buf, size_t out_size) {
|
|
if (!out_buf || out_size == 0) return;
|
|
out_buf[0] = '\0';
|
|
|
|
const char *caller_ld = NULL;
|
|
if (envp) {
|
|
for (int i = 0; envp[i] != NULL; i++) {
|
|
if (envp[i][0] == 'L' && strncmp(envp[i], "LD_LIBRARY_PATH=", 16) == 0) {
|
|
caller_ld = envp[i] + 16;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!caller_ld) {
|
|
caller_ld = getenv("LD_LIBRARY_PATH");
|
|
}
|
|
int has_runpath = runpath && runpath[0];
|
|
char conf[PATH_MAX * 2];
|
|
ldconf_dirs(conf, sizeof(conf));
|
|
// The rootfs defaults that The native userland's shells export in LD_LIBRARY_PATH go to the end, after the
|
|
// program's and ld.so.conf's own directories, as on Linux (where LD_LIBRARY_PATH has none of them)
|
|
int move_defaults = has_runpath || conf[0] != '\0';
|
|
|
|
size_t used = 0;
|
|
if (has_runpath && is_rpath) used = append_runpath_dirs(out_buf, out_size, used, runpath, exe);
|
|
|
|
// The caller's directories (those inside the rootfs rewritten), written straight into out_buf:
|
|
// this runs in nearly every child a shell forks, where two 8 KB copies of the list on the stack
|
|
// were pages more to copy before the exec.
|
|
size_t cap = used + PATH_MAX * 2 < out_size ? used + PATH_MAX * 2 : out_size; // the caller's part, as before
|
|
for (const char *p = caller_ld; p && *p;) {
|
|
const char *end = strchr(p, ':');
|
|
size_t len = end ? (size_t)(end - p) : strlen(p);
|
|
if (len > 0 && len < PATH_MAX) {
|
|
char tok[PATH_MAX];
|
|
memcpy(tok, p, len);
|
|
tok[len] = '\0';
|
|
char rw_buf[PATH_MAX];
|
|
const char *rw = (tok[0] == '/') ? rewrite_path_in_root(tok, rw_buf, sizeof(rw_buf)) : tok;
|
|
size_t rw_len = strlen(rw);
|
|
size_t need = (used > 0 ? 1 : 0) + rw_len;
|
|
if (move_defaults && is_rootfs_default_lib_dir(rw, rw_len)) {
|
|
// after the program's own directories, with the other defaults
|
|
} else if (used + need < cap) {
|
|
if (used > 0) out_buf[used++] = ':';
|
|
memcpy(out_buf + used, rw, rw_len);
|
|
used += rw_len;
|
|
out_buf[used] = '\0';
|
|
}
|
|
}
|
|
if (!end) break;
|
|
p = end + 1;
|
|
}
|
|
if (has_runpath && !is_rpath) used = append_runpath_dirs(out_buf, out_size, used, runpath, exe);
|
|
// ld.so.conf, as ld.so's cache: after LD_LIBRARY_PATH and RUNPATH, before the default directories
|
|
for (const char *p = conf; *p;) {
|
|
const char *end = strchr(p, ':');
|
|
size_t len = end ? (size_t)(end - p) : strlen(p);
|
|
used = append_lib_dir(out_buf, out_size, used, p, len);
|
|
if (!end) break;
|
|
p = end + 1;
|
|
}
|
|
append_rootfs_lib_dirs_missing_from(out_buf, out_size, used, used > 0, out_buf);
|
|
}
|
|
|
|
// fstat() without the hook's stat emulation: the probe wants the mode bits only, and the
|
|
// emulation would map the hard link table and look up the app's ids in a child that is about to
|
|
// exec (new pages there, each a copy-on-write fault). Resolved by prepare_exec_in_parent() when the
|
|
// process forks, so that its children find it set.
|
|
static int (*g_probe_fstat)(int, struct stat *) = NULL;
|
|
|
|
static int probe_fstat(int fd, struct stat *st) {
|
|
int (*f)(int, struct stat *) = __atomic_load_n(&g_probe_fstat, __ATOMIC_RELAXED);
|
|
if (!f) {
|
|
f = (int (*)(int, struct stat *))dlsym(RTLD_NEXT, "fstat");
|
|
if (!f) return fstat(fd, st);
|
|
__atomic_store_n(&g_probe_fstat, f, __ATOMIC_RELAXED);
|
|
}
|
|
return f(fd, st);
|
|
}
|
|
|
|
static void probe_exec_file(const char *path, struct exec_probe *p) {
|
|
p->hdr[0] = '\0';
|
|
p->n = -1;
|
|
p->is_elf = 0;
|
|
p->is_dynamic = 0;
|
|
p->android_interp = 0;
|
|
p->e_machine = 0;
|
|
p->mode = 0;
|
|
p->runpath[0] = '\0';
|
|
p->runpath_is_rpath = 0;
|
|
if (!path || path[0] == '\0') return;
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) return;
|
|
ssize_t n = read(fd, p->hdr, sizeof(p->hdr) - 1);
|
|
p->n = n;
|
|
p->hdr[n > 0 ? n : 0] = '\0';
|
|
const unsigned char *h = (const unsigned char *)p->hdr;
|
|
if (n >= 4 && h[0] == 0x7f && h[1] == 'E' && h[2] == 'L' && h[3] == 'F') {
|
|
p->is_elf = 1;
|
|
if (n >= 20) p->e_machine = (uint16_t)(h[18] | (h[19] << 8));
|
|
int dynamic = elf_interp_in_buffer(h, n);
|
|
if (dynamic < 0) dynamic = check_elf_dynamic(fd, h, n);
|
|
p->is_dynamic = dynamic;
|
|
if (dynamic && p->e_machine == 183 /* EM_AARCH64 */) p->android_interp = elf_interp_is_android(fd, h, n);
|
|
if (dynamic && !p->android_interp) p->runpath_is_rpath = elf_read_runpath(fd, h, n, p->runpath, sizeof(p->runpath)) == 2;
|
|
struct stat st;
|
|
if (dynamic && probe_fstat(fd, &st) == 0) p->mode = st.st_mode;
|
|
}
|
|
close(fd);
|
|
}
|
|
|
|
static int is_ld_linux(const char *path) {
|
|
if (!path) return 0;
|
|
const char *base = strrchr(path, '/');
|
|
base = base ? base + 1 : path;
|
|
return (strncmp(base, "ld-linux", 8) == 0 || strncmp(base, "ld-2.", 5) == 0);
|
|
}
|
|
|
|
// Splits a "#!" line (NUL-terminated `hdr`, modified in place) into the interpreter and its
|
|
// optional argument, the way execve() always has.
|
|
static void parse_shebang(char *hdr, char **interp, char **interp_arg) {
|
|
char *newline = strchr(hdr, '\n');
|
|
if (newline) *newline = '\0';
|
|
char *p = hdr + 2;
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
char *arg = strchr(p, ' ');
|
|
if (arg) {
|
|
*arg = '\0';
|
|
arg++;
|
|
while (*arg == ' ' || *arg == '\t') arg++;
|
|
}
|
|
*interp = p;
|
|
*interp_arg = arg;
|
|
}
|
|
|
|
// Interpreters outside the rootfs (Android's /system/bin/sh) run with Android's environment.
|
|
static int is_host_interpreter(const char *interp) {
|
|
return strncmp(interp, "/system", 7) == 0 ||
|
|
(g_ddnatv_root[0] != '\0' && strncmp(interp, g_ddnatv_root, g_ddnatv_root_len) != 0);
|
|
}
|
|
|
|
// argv for running `script` with its interpreter: interp [arg] script args... (the script's own
|
|
// argv[0] is dropped, as the kernel does). free() the result.
|
|
static char **build_script_argv(const char *interp, const char *interp_arg, const char *script, char *const argv[]) {
|
|
int argc = 0;
|
|
while (argv && argv[argc]) argc++;
|
|
int has_arg = (interp_arg && *interp_arg) ? 1 : 0;
|
|
char **v = (char **)calloc(argc + has_arg + 3, sizeof(char *));
|
|
if (!v) return NULL;
|
|
int n = 0;
|
|
v[n++] = (char *)interp;
|
|
if (has_arg) v[n++] = (char *)interp_arg;
|
|
v[n++] = (char *)script;
|
|
for (int i = 1; i < argc; i++) v[n++] = argv[i];
|
|
v[n] = NULL;
|
|
return v;
|
|
}
|
|
|
|
// argv for "ld.so [--preload hook] --library-path dirs --argv0 name program args...".
|
|
// free() the result; the strings stay owned by the caller.
|
|
static char **build_ldso_argv(const char *ld_so, const char *hook, const char *lib_path,
|
|
const char *program, const char *prog_name, char *const argv[]) {
|
|
int argc = 0;
|
|
while (argv && argv[argc]) argc++;
|
|
char **v = (char **)calloc(argc + 16, sizeof(char *));
|
|
if (!v) return NULL;
|
|
int n = 0;
|
|
v[n++] = (char *)ld_so;
|
|
if (hook) {
|
|
v[n++] = (char *)"--preload";
|
|
v[n++] = (char *)hook;
|
|
}
|
|
v[n++] = (char *)"--library-path";
|
|
v[n++] = (char *)lib_path;
|
|
v[n++] = (char *)"--argv0";
|
|
v[n++] = (char *)((argc > 0 && argv[0]) ? argv[0] : prog_name);
|
|
v[n++] = (char *)program;
|
|
for (int i = 1; i < argc; i++) v[n++] = argv[i];
|
|
v[n] = NULL;
|
|
return v;
|
|
}
|
|
|
|
// POSIX_SPAWN_RESETIDS makes glibc's spawn child call setresuid() and setresgid() after it has reset
|
|
// every signal handler to its default, which includes the SIGSYS handler that emulates the calls
|
|
// Android's seccomp filter blocks, so the child died of SIGSYS (GNU make asks for the flag on every
|
|
// command it starts). The native userland shows one fixed user id and never runs set-user-id programs: there is
|
|
// no id to reset, so the flag is left out and everything else is passed on.
|
|
static const posix_spawnattr_t *spawn_attr_for_android(const posix_spawnattr_t *attrp, posix_spawnattr_t *copy) {
|
|
short flags = 0;
|
|
if (!attrp || posix_spawnattr_getflags(attrp, &flags) != 0 || !(flags & POSIX_SPAWN_RESETIDS)) return attrp;
|
|
memcpy(copy, attrp, sizeof(*copy));
|
|
posix_spawnattr_setflags(copy, (short)(flags & ~POSIX_SPAWN_RESETIDS));
|
|
return copy;
|
|
}
|