mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
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.
75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
|
|
|
|
PROGRAM = r"""
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <sys/mman.h>
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 2) return 2;
|
|
int fd = open(argv[1], O_RDONLY);
|
|
void *code = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0);
|
|
printf("map %d\n", code == MAP_FAILED ? errno : 0);
|
|
void *data = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
printf("protect %d\n", mprotect(data, 4096, PROT_READ | PROT_EXEC) ? errno : 0);
|
|
printf("read %c\n", code == MAP_FAILED ? '-' : *(char *)code);
|
|
return 0;
|
|
}
|
|
"""
|
|
|
|
|
|
def noexec_folder():
|
|
for folder in ("/run/lock", "/dev/shm", "/run/user/%d" % os.getuid()):
|
|
try:
|
|
if os.access(folder, os.W_OK) and os.statvfs(folder).f_flag & os.ST_NOEXEC:
|
|
return folder
|
|
except OSError:
|
|
pass
|
|
return None
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("cc") and noexec_folder(), "needs cc and a writable noexec mount")
|
|
class NoexecPreloadTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.tmp = Path(tempfile.mkdtemp())
|
|
cls.lib = cls.tmp / "libnoexec.so"
|
|
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-pthread", "-DNOEXEC_TEST", "-o", str(cls.lib),
|
|
str(PRELOAD / "noexec.c"), str(PRELOAD / "fsync.c"), str(PRELOAD / "robust.c"), str(PRELOAD / "next.c"), "-ldl"], check=True)
|
|
source = cls.tmp / "program.c"
|
|
source.write_text(PROGRAM)
|
|
for name in ("FEX", "game"):
|
|
subprocess.run(["cc", "-O2", "-o", str(cls.tmp / name), str(source)], check=True)
|
|
cls.target = Path(tempfile.mkdtemp(dir=noexec_folder())) / "Game.x86_64"
|
|
cls.target.write_bytes(b"\x7fELF" + bytes(4092))
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
shutil.rmtree(cls.tmp, ignore_errors=True)
|
|
shutil.rmtree(cls.target.parent, ignore_errors=True)
|
|
|
|
def run_as(self, name, preload=True):
|
|
env = dict(os.environ, LD_PRELOAD=str(self.lib)) if preload else {k: v for k, v in os.environ.items() if k != "LD_PRELOAD"}
|
|
result = subprocess.run([str(self.tmp / name), str(self.target)], env=env, capture_output=True, text=True, check=True)
|
|
return result.stdout.split("\n")[:3]
|
|
|
|
def test_fex_maps_a_program_on_noexec_storage(self):
|
|
self.assertEqual(self.run_as("FEX"), ["map 0", "protect 0", "read \x7f"])
|
|
|
|
def test_the_kernel_still_refuses_everyone_else(self):
|
|
for refused in (self.run_as("game"), self.run_as("FEX", preload=False)):
|
|
self.assertIn(refused[0], ("map 1", "map 13"))
|
|
self.assertEqual(refused[1:], ["protect 13", "read -"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|