mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +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.
161 lines
6.7 KiB
Python
161 lines
6.7 KiB
Python
import errno
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload"
|
|
|
|
PROGRAM = r"""
|
|
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <spawn.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
extern char **environ;
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 3) return 2;
|
|
const char *how = argv[1], *file = argv[2];
|
|
char **args = argv + 2;
|
|
if (!strcmp(how, "access")) {
|
|
int result = access(file, X_OK);
|
|
printf("access %d %d\n", result, result ? errno : 0);
|
|
return 0;
|
|
}
|
|
if (!strcmp(how, "spawn") || !strcmp(how, "spawnp")) {
|
|
pid_t pid;
|
|
int error = how[5] ? posix_spawnp(&pid, file, NULL, NULL, args, environ) : posix_spawn(&pid, file, NULL, NULL, args, environ);
|
|
if (error) { printf("errno %d\n", error); return 3; }
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
printf("exit %d\n", WEXITSTATUS(status));
|
|
return 0;
|
|
}
|
|
if (!strcmp(how, "execve")) execve(file, args, environ);
|
|
else if (!strcmp(how, "execv")) execv(file, args);
|
|
else if (!strcmp(how, "execvp")) execvp(file, args);
|
|
else if (!strcmp(how, "execvpe")) execvpe(file, args, environ);
|
|
printf("errno %d\n", errno);
|
|
return 3;
|
|
}
|
|
"""
|
|
|
|
# Shell builtins only: anything it ran would be an x86 program, handed back to it.
|
|
HANDLER = "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$HANDLER_LOG\"\nexit 7\n"
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("gcc") and os.uname().machine == "x86_64", "needs gcc on an x86-64 host")
|
|
class BinfmtPreloadTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.tmp = Path(tempfile.mkdtemp())
|
|
cls.handler = cls.tmp / "handler"
|
|
cls.handler.write_text(HANDLER)
|
|
cls.handler.chmod(0o755)
|
|
cls.library = cls.tmp / "libbinfmt.so"
|
|
subprocess.run(["gcc", "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-Werror", "-DBINFMT_TEST",
|
|
'-DBINFMT_HANDLER="%s"' % cls.handler, "-o", str(cls.library),
|
|
"-pthread"] + [str(PRELOAD / name) for name in ("binfmt.c", "net.c", "udevmon.c", "pathcache.c", "next.c")] + ["-ldl"],
|
|
check=True)
|
|
source = cls.tmp / "program.c"
|
|
source.write_text(PROGRAM)
|
|
cls.program = cls.tmp / "program"
|
|
subprocess.run(["gcc", "-O2", "-o", str(cls.program), str(source)], check=True)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
shutil.rmtree(cls.tmp, True)
|
|
|
|
def setUp(self):
|
|
self.dir = Path(tempfile.mkdtemp(dir=self.tmp))
|
|
self.log = self.dir / "handler.log"
|
|
|
|
def file(self, name, data, mode=0o755):
|
|
path = self.dir / name
|
|
path.write_bytes(data)
|
|
path.chmod(mode)
|
|
return path
|
|
|
|
def x86(self, name="tool", mode=0o755):
|
|
path = self.dir / name
|
|
shutil.copyfile(shutil.which("true"), path)
|
|
path.chmod(mode)
|
|
return path
|
|
|
|
def run_program(self, how, target, *args, desktop=True):
|
|
env = {"PATH": "%s:/usr/bin:/bin" % self.dir, "LD_PRELOAD": str(self.library), "HANDLER_LOG": str(self.log)}
|
|
if desktop:
|
|
env["BL_DESKTOP"] = "1"
|
|
done = subprocess.run([str(self.program), how, str(target)] + list(args), env=env,
|
|
stdout=subprocess.PIPE, text=True, timeout=30)
|
|
handed = self.log.read_text().splitlines() if self.log.exists() else None
|
|
return done.returncode, done.stdout.strip(), handed
|
|
|
|
def test_an_x86_program_goes_to_the_handler_before_it_is_executed(self):
|
|
tool = self.x86()
|
|
for how in ("execve", "execv"):
|
|
self.log.unlink(missing_ok=True)
|
|
self.assertEqual(self.run_program(how, tool, "a", "b c"), (7, "", ["--exec", str(tool), "a", "b c"]))
|
|
|
|
def test_outside_the_desktop_nothing_changes(self):
|
|
self.assertEqual(self.run_program("execve", self.x86(), desktop=False), (0, "", None))
|
|
|
|
def test_a_program_without_execute_permission_stays_refused(self):
|
|
tool = self.x86(mode=0o644)
|
|
self.assertEqual(self.run_program("execve", tool), (3, "errno %d" % errno.EACCES, None))
|
|
|
|
def test_a_native_program_the_kernel_cannot_run_keeps_its_error(self):
|
|
arm64 = bytearray(64)
|
|
arm64[0:4], arm64[4], arm64[5], arm64[18] = b"\x7fELF", 2, 1, 183
|
|
program = self.file("arm64", bytes(arm64))
|
|
self.assertEqual(self.run_program("execve", program), (3, "errno %d" % errno.ENOEXEC, None))
|
|
|
|
def test_a_script_runs_as_itself(self):
|
|
script = self.file("script.sh", b"#!/bin/sh\nexit 5\n")
|
|
self.assertEqual(self.run_program("execve", script), (5, "", None))
|
|
|
|
def test_a_windows_program_found_on_path_goes_to_the_handler(self):
|
|
program = self.file("setup.exe", b"MZ" + bytes(62))
|
|
for how in ("execvp", "execvpe"):
|
|
self.log.unlink(missing_ok=True)
|
|
self.assertEqual(self.run_program(how, "setup.exe", "/S"), (7, "", ["--exec", str(program), "/S"]))
|
|
|
|
def test_a_spawned_x86_program_goes_to_the_handler(self):
|
|
tool = self.x86()
|
|
self.assertEqual(self.run_program("spawn", tool, "x"), (0, "exit 7", ["--exec", str(tool), "x"]))
|
|
self.log.unlink()
|
|
self.assertEqual(self.run_program("spawnp", "tool", "y"), (0, "exit 7", ["--exec", str(tool), "y"]))
|
|
|
|
def test_access_keeps_the_kernels_answer_off_noexec_storage(self):
|
|
self.assertEqual(self.run_program("access", self.x86(mode=0o644)), (0, "access -1 %d" % errno.EACCES, None))
|
|
self.assertEqual(self.run_program("access", self.x86()), (0, "access 0 0", None))
|
|
|
|
def test_a_program_on_noexec_storage_passes_access(self):
|
|
storage = next((d for d in (os.environ.get("XDG_RUNTIME_DIR"), "/dev/shm", "/run/user/%d" % os.getuid())
|
|
if d and os.path.isdir(d) and os.access(d, os.W_OK) and os.statvfs(d).f_flag & os.ST_NOEXEC), None)
|
|
if storage is None:
|
|
self.skipTest("no writable noexec mount here")
|
|
folder = Path(tempfile.mkdtemp(dir=storage))
|
|
self.addCleanup(shutil.rmtree, folder, True)
|
|
tool = folder / "tool"
|
|
shutil.copyfile(shutil.which("true"), tool)
|
|
notes = folder / "notes.txt"
|
|
notes.write_text("hello\n")
|
|
self.assertEqual(self.run_program("access", tool), (0, "access 0 0", None))
|
|
self.assertEqual(self.run_program("access", notes), (0, "access -1 %d" % errno.EACCES, None))
|
|
self.assertEqual(self.run_program("access", tool, desktop=False), (0, "access -1 %d" % errno.EACCES, None))
|
|
|
|
def test_a_spawned_native_script_is_left_alone(self):
|
|
self.file("script.sh", b"#!/bin/sh\nexit 5\n")
|
|
self.assertEqual(self.run_program("spawnp", "script.sh"), (0, "exit 5", None))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|