Files
maxjivi05 8a6877423e AppImage apps: Electron programs such as Heroic open under FEX
Every Electron AppImage hung without a window or crashed at its first page.
Four causes, each enough on its own:

- The session preload's hooks found the real functions with
  dlsym(RTLD_NEXT). Before glibc 2.34 a thread's first dlsym callocs, and
  Chromium's PartitionAlloc calls syscall() from inside its first malloc:
  the allocator waited for itself. On older glibc (the x86 runtime's 2.31)
  next.c now walks the libraries after the preload with no allocation and
  picks each function's default version, as glibc 2.36 does; the old
  lookup handed realpath() and posix_spawn() their compat versions, and
  realpath(path, NULL) failed. arm64 keeps dlsym.
- libpci's sysfs backend exits when /sys/bus/pci/devices is listable but a
  device's files are not (a RedMagic's PCIe root port), taking every
  Chromium GPU process with it. An empty directory is bound over it on
  devices where that happens.
- FEX_OUTPUTLOG=stderr made FEX log to its own duplicate of descriptor 2.
  Chromium dup2()s its IPC socket onto that low number in each child it
  starts, then lists /proc/self/fd, which FEX logs into the socket. Empty,
  FEX writes to descriptor 2 itself.
- The tracer preload served every /proc/<pid>/status as a memfd, which is
  open for writing. Chromium hands each renderer its status as a read-only
  file and checks that it is one. Only a process's own status (pid, tid,
  self, thread-self) is rewritten now.
2026-10-11 00:17:55 -04:00

111 lines
4.0 KiB
C

/*
* next.c: the definition a hook passes a call on to, as dlsym(RTLD_NEXT, name) finds it.
*
* Before glibc 2.34 a thread's first dlsym callocs dlerror's state. A hook first called from inside
* an allocator that is still starting up - Chromium's PartitionAlloc calls syscall() from its first
* malloc - re-enters the allocator there, and it waits for itself forever: every Electron and
* Chromium program on Steam's x86 runtime (glibc 2.31) hung at start. There the libraries after this
* one are searched by hand, with nothing allocated; an object or symbol that walk cannot handle is
* left to dlsym.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdint.h>
#include <string.h>
/* NEXT_TEST: tools/tests builds the walk on a newer glibc. */
#if __GLIBC_PREREQ(2, 34) && !defined(NEXT_TEST)
__attribute__((visibility("hidden"))) void *bl_next(const char *name) {
return dlsym(RTLD_NEXT, name);
}
#else
struct search {
const char *name;
uint32_t hash;
int passed;
int unsure;
void *found;
};
static uint32_t gnu_hash(const char *name) {
uint32_t h = 5381;
for (; *name; name++) h = h * 33 + (unsigned char)*name;
return h;
}
/* glibc relocates these entries in place, but not every object's (the vDSO's). */
static ElfW(Addr) address(const struct dl_phdr_info *info, ElfW(Addr) value) {
return value < info->dlpi_addr ? info->dlpi_addr + value : value;
}
/* The object's definition, as glibc 2.36 and later pick it: unversioned or the default version. */
static void find(const struct dl_phdr_info *info, const ElfW(Dyn) *dyn, struct search *s) {
const uint32_t *table = NULL;
const ElfW(Sym) *symbols = NULL;
const char *strings = NULL;
const ElfW(Half) *versions = NULL;
for (; dyn->d_tag != DT_NULL; dyn++) {
if (dyn->d_tag == DT_GNU_HASH) table = (const uint32_t *)address(info, dyn->d_un.d_ptr);
else if (dyn->d_tag == DT_SYMTAB) symbols = (const ElfW(Sym) *)address(info, dyn->d_un.d_ptr);
else if (dyn->d_tag == DT_STRTAB) strings = (const char *)address(info, dyn->d_un.d_ptr);
else if (dyn->d_tag == DT_VERSYM) versions = (const ElfW(Half) *)address(info, dyn->d_un.d_ptr);
}
if (!table || !symbols || !strings || !table[0]) {
s->unsure = 1;
return;
}
const uint32_t *buckets = (const uint32_t *)((const ElfW(Addr) *)(table + 4) + table[2]);
const uint32_t *chain = buckets + table[0] - table[1];
const ElfW(Sym) *only = NULL;
int matches = 0, defaults = 0;
for (uint32_t i = buckets[s->hash % table[0]]; i >= table[1] && i != 0; i++) {
const ElfW(Sym) *sym = &symbols[i];
if ((chain[i] | 1) == (s->hash | 1) && sym->st_shndx != SHN_UNDEF && sym->st_value != 0 &&
ELF64_ST_BIND(sym->st_info) != STB_LOCAL && !strcmp(strings + sym->st_name, s->name)) {
if (ELF64_ST_TYPE(sym->st_info) == STT_GNU_IFUNC || ELF64_ST_TYPE(sym->st_info) == STT_TLS) {
s->unsure = 1;
return;
}
if (!versions || (versions[i] & 0x7fff) < 2) {
only = sym;
defaults = 1;
break;
}
matches++;
if (!(versions[i] & 0x8000) && defaults++ == 0) only = sym;
}
if (chain[i] & 1) break;
}
/* Only hidden versions: dlsym's own rules decide. */
if (defaults == 1) s->found = (void *)(info->dlpi_addr + only->st_value);
else if (matches) s->unsure = 1;
}
static int visit(struct dl_phdr_info *info, size_t size, void *data) {
struct search *s = data;
const ElfW(Dyn) *dyn = NULL;
(void)size;
for (int i = 0; i < info->dlpi_phnum; i++)
if (info->dlpi_phdr[i].p_type == PT_DYNAMIC) dyn = (const ElfW(Dyn) *)(info->dlpi_addr + info->dlpi_phdr[i].p_vaddr);
if (!dyn) return 0;
if (!s->passed) {
s->passed = dyn == _DYNAMIC;
return 0;
}
find(info, dyn, s);
return s->found || s->unsure;
}
__attribute__((visibility("hidden"))) void *bl_next(const char *name) {
struct search s = {name, gnu_hash(name), 0, 0, NULL};
dl_iterate_phdr(visit, &s);
if (s.found || (s.passed && !s.unsure)) return s.found;
return dlsym(RTLD_NEXT, name);
}
#endif