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

68 lines
2.3 KiB
C

/*
* noexec.c: FEX runs an x86 program and its libraries where they lie, on noexec shared storage too.
* FEX maps them with the guest's PROT_EXEC, which the kernel refuses for a file on a noexec mount,
* but it never runs those pages itself - it runs its own translation of them - so a mapping or an
* mprotect refused for that alone is made without PROT_EXEC. Only in FEX's own processes.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/statvfs.h>
#include <unistd.h>
__attribute__((visibility("hidden"))) void *bl_next(const char *name);
/* NOEXEC_TEST: tools/tests builds it for the host. */
#if defined(__aarch64__) || defined(NOEXEC_TEST)
static int in_fex(void) {
static int known = -1;
if (known < 0) {
char exe[512];
ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1);
exe[n > 0 ? n : 0] = 0;
const char *name = strrchr(exe, '/');
name = name ? name + 1 : exe;
known = !strcmp(name, "FEX") || !strcmp(name, "FEXInterpreter") || !strcmp(name, "FEXLoader");
}
return known;
}
/* After an mmap failed (fsync.c's mmap asks): whether to map it again without PROT_EXEC. */
__attribute__((visibility("hidden"))) int bl_noexec_retry(int prot, int flags, int fd) {
int error = errno;
struct statvfs vfs;
int retry = (error == EACCES || error == EPERM) && (prot & PROT_EXEC) && fd >= 0 && !(flags & MAP_ANONYMOUS) &&
in_fex() && fstatvfs(fd, &vfs) == 0 && (vfs.f_flag & ST_NOEXEC);
errno = error;
return retry;
}
static int (*real_mprotect)(void *, size_t, int);
/* Looked up before anything runs: Wine calls mprotect from its signal handlers, where dlsym is unsafe. */
__attribute__((constructor)) static void noexec_init(void) {
real_mprotect = (int (*)(void *, size_t, int))bl_next("mprotect");
}
int mprotect(void *addr, size_t len, int prot) {
if (!real_mprotect) noexec_init();
int result = real_mprotect(addr, len, prot);
if (result && errno == EACCES && (prot & PROT_EXEC) && in_fex()) {
result = real_mprotect(addr, len, prot & ~PROT_EXEC);
if (result) errno = EACCES;
}
return result;
}
#else
__attribute__((visibility("hidden"))) int bl_noexec_retry(int prot, int flags, int fd) {
(void)prot, (void)flags, (void)fd;
return 0;
}
#endif