diff --git a/README.md b/README.md index 7515dffdd..fb86fa653 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Compatibility and performance vary by device; hardware validation is limited. De ## Credits and licence -GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. The registry entries a GOG game's install script (`goggame-.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc//mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve. +GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. The registry entries a GOG game's install script (`goggame-.script`) sets are read the way [Heroic Games Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher)'s GOG setup (GPL-3.0) reads them. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence, which also starts the arm64 emulator AppImages whose own runtime cannot run its extractor on Android. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). x86 programs that link OpenSSL 3 under FEX use the copy in the [Steam Linux Runtime 4.0](https://gitlab.steamos.cloud/steamrt/steamrt) platform. Hooks written with `WriteProcessMemory` into code [FEX](https://github.com/FEX-Emu/FEX) has translated go through `/proc//mem` again when the kernel refuses `process_vm_writev`, as [Wine](https://gitlab.winehq.org/wine/wine)'s `server/ptrace.c` wrote them before Wine 11. x86 Linux games under FEX get the same `/etc/asound.conf` that [pressure-vessel](https://gitlab.steamos.cloud/steamrt/steam-runtime-tools) writes into the Steam Linux Runtime, so ALSA plays through PulseAudio. Games render with [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's own per-render-pass choice between tile memory and system memory, while the Steam client keeps `TU_DEBUG=sysmem`; [FEX](https://github.com/FEX-Emu/FEX)'s code disk cache was checked in its source and is already on through Proton. The proot fast path picks the guest name of a host file bound under several names in the order [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later) sorts its bindings, so links and `/proc/self/fd` read back as they do through proot. The session's [PipeWire](https://gitlab.freedesktop.org/pipewire/pipewire) (MIT) opens its sockets with the per-socket access settings documented in its `module-access.c`, and [WirePlumber](https://gitlab.freedesktop.org/pipewire/wireplumber) (MIT) keeps those clients at full permissions through its `access.rules`. Wine write watches use the userfaultfd support in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/ntdll/unix/virtual.c` (LGPL-2.1-or-later): libblsession gives it the arm64 syscall number and answers its `PAGEMAP_SCAN` from `/proc/self/pagemap` where Android refuses the ioctl. Wine on arm64 sets a thread's full context by raising `SIGUSR2` from `syscall_dispatcher_return_slowpath` in [Wine](https://gitlab.winehq.org/wine/wine)'s `dlls/ntdll/unix/signal_arm64.c` (LGPL-2.1-or-later); libblsession runs that handler in place on a frame laid out as the [Linux](https://github.com/torvalds/linux) kernel's `arch/arm64/kernel/signal.c` builds it and returns through `rt_sigreturn`, so proot does not stop the thread for it. Wine's process list reads each process's memory counters from `/proc//status` in `fill_vm_counters` ([Wine](https://gitlab.winehq.org/wine/wine) `dlls/ntdll/unix/process.c`, LGPL-2.1-or-later), so libblsession gives those reads the kernel's file instead of a copy with `TracerPid` cleared. See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve. XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0); see [docs/third-party/rayneo](docs/third-party/rayneo/README.md). XREAL 1S support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). Rokid support follows the Rokid protocol notes in [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) and @badicsalex's [Void Computing write-ups](https://voidcomputing.hu/blog/worse-better-prettier/), and [Monado](https://gitlab.freedesktop.org/monado/monado)'s Rokid driver. The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL, Rokid and REDMAGIC are trademarks of their owners; this project is not affiliated with them. diff --git a/tools/linuxfs/preload/tracer.c b/tools/linuxfs/preload/tracer.c index 7d8c741c4..572cf13e0 100644 --- a/tools/linuxfs/preload/tracer.c +++ b/tools/linuxfs/preload/tracer.c @@ -16,6 +16,8 @@ #define STATUS_MAX 16384 #define TRACER_FIELD "\nTracerPid:\t" +int bl_from_ntdll(const void *caller) __attribute__((visibility("hidden"))); + static const char *skip_digits(const char *s) { const char *end = s; while (isdigit((unsigned char)*end)) end++; @@ -77,7 +79,8 @@ __attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *p int out = -1; long pid; - if ((flags & O_ACCMODE) != O_RDONLY || (pid = status_pid(path)) < 0 || is_web_helper(caller)) return -1; + if ((flags & O_ACCMODE) != O_RDONLY || (pid = status_pid(path)) < 0 || is_web_helper(caller) || bl_from_ntdll(caller)) + return -1; int in = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC); if (in < 0) goto done; for (;;) { diff --git a/tools/linuxfs/preload/usr2.c b/tools/linuxfs/preload/usr2.c index 049518c28..a7129fdc0 100644 --- a/tools/linuxfs/preload/usr2.c +++ b/tools/linuxfs/preload/usr2.c @@ -10,6 +10,34 @@ #include #include +static uintptr_t ntdll_lo, ntdll_hi; + +static int find_ntdll(struct dl_phdr_info *info, size_t size, void *data) { + (void)size; + (void)data; + const char *slash = strrchr(info->dlpi_name, '/'); + if (strcmp(slash ? slash + 1 : info->dlpi_name, "ntdll.so") != 0) return 0; + for (int i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; + if (ph->p_type != PT_LOAD || !(ph->p_flags & PF_X)) continue; + uintptr_t lo = info->dlpi_addr + ph->p_vaddr; + __atomic_store_n(&ntdll_hi, lo + ph->p_memsz, __ATOMIC_RELAXED); + __atomic_store_n(&ntdll_lo, lo, __ATOMIC_RELEASE); + return 1; + } + return 0; +} + +__attribute__((visibility("hidden"))) int bl_from_ntdll(const void *caller) { + uintptr_t pc = (uintptr_t)caller, lo = __atomic_load_n(&ntdll_lo, __ATOMIC_ACQUIRE); + if (!lo) { + dl_iterate_phdr(find_ntdll, NULL); + lo = __atomic_load_n(&ntdll_lo, __ATOMIC_ACQUIRE); + if (!lo) return 0; + } + return pc >= lo && pc < __atomic_load_n(&ntdll_hi, __ATOMIC_RELAXED); +} + #if defined(__aarch64__) #include #include @@ -126,34 +154,6 @@ static long usr2_sys(long nr, long a, long b, long c, long d) { return x0; } -static uintptr_t usr2_lo, usr2_hi; - -static int usr2_find_ntdll(struct dl_phdr_info *info, size_t size, void *data) { - (void)size; - (void)data; - const char *slash = strrchr(info->dlpi_name, '/'); - if (strcmp(slash ? slash + 1 : info->dlpi_name, "ntdll.so") != 0) return 0; - for (int i = 0; i < info->dlpi_phnum; i++) { - const ElfW(Phdr) *ph = &info->dlpi_phdr[i]; - if (ph->p_type != PT_LOAD || !(ph->p_flags & PF_X)) continue; - uintptr_t lo = info->dlpi_addr + ph->p_vaddr; - __atomic_store_n(&usr2_hi, lo + ph->p_memsz, __ATOMIC_RELAXED); - __atomic_store_n(&usr2_lo, lo, __ATOMIC_RELEASE); - return 1; - } - return 0; -} - -static int usr2_from_ntdll(void *caller) { - uintptr_t pc = (uintptr_t)caller, lo = __atomic_load_n(&usr2_lo, __ATOMIC_ACQUIRE); - if (!lo) { - dl_iterate_phdr(usr2_find_ntdll, NULL); - lo = __atomic_load_n(&usr2_lo, __ATOMIC_ACQUIRE); - if (!lo) return 0; - } - return pc >= lo && pc < __atomic_load_n(&usr2_hi, __ATOMIC_RELAXED); -} - static uint64_t usr2_ssbs(void) { uint64_t value = 0; if (getauxval(AT_HWCAP) & HWCAP_SSBS) __asm__ volatile("mrs %0, s3_3_c4_c2_6" : "=r"(value)); @@ -192,7 +192,7 @@ int raise(int sig) { real = (int (*)(int))dlsym(RTLD_NEXT, "raise"); __atomic_store_n(&usr2_real_raise, real, __ATOMIC_RELEASE); } - if (sig != SIGUSR2 || (getauxval(AT_HWCAP) & BL_HWCAP_GCS) || !usr2_from_ntdll(__builtin_return_address(0))) { + if (sig != SIGUSR2 || (getauxval(AT_HWCAP) & BL_HWCAP_GCS) || !bl_from_ntdll(__builtin_return_address(0))) { return real(sig); } diff --git a/tools/tests/test_tracer_preload.py b/tools/tests/test_tracer_preload.py new file mode 100644 index 000000000..90a444224 --- /dev/null +++ b/tools/tests/test_tracer_preload.py @@ -0,0 +1,154 @@ +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +PRELOAD = Path(__file__).resolve().parents[1] / "linuxfs/preload" + +NTDLL = r""" +#define _GNU_SOURCE +#include +#include +#include +#include + +__attribute__((noinline)) int ntdll_open(const char *path) { + int fd = open(path, O_RDONLY | O_CLOEXEC); + __asm__ volatile("" ::: "memory"); + return fd; +} + +__attribute__((noinline)) int ntdll_openat(const char *path) { + int fd = openat(AT_FDCWD, path, O_RDONLY | O_CLOEXEC); + __asm__ volatile("" ::: "memory"); + return fd; +} + +__attribute__((noinline)) FILE *ntdll_fopen(const char *path) { + FILE *f = fopen(path, "r"); + __asm__ volatile("" ::: "memory"); + return f; +} +""" + +PROGRAM = r""" +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +int ntdll_open(const char *path); +int ntdll_openat(const char *path); +FILE *ntdll_fopen(const char *path); + +static void report(const char *name, int fd) { + char link[64], target[256] = "", text[16384], *field; + ssize_t n; + long tracer = -1; + if (fd < 0) { + printf("%s failed\n", name); + return; + } + snprintf(link, sizeof(link), "/proc/self/fd/%d", fd); + n = readlink(link, target, sizeof(target) - 1); + target[n > 0 ? n : 0] = '\0'; + n = pread(fd, text, sizeof(text) - 1, 0); + text[n > 0 ? n : 0] = '\0'; + if ((field = strstr(text, "\nTracerPid:\t")) != NULL) tracer = strtol(field + 12, NULL, 10); + const char *kind = strncmp(target, "/proc/", 6) == 0 ? "proc" + : strncmp(target, "/memfd:", 7) == 0 ? "memfd" + : strncmp(target, "/tmp/.droiddeck-status-", 23) == 0 ? "copy" : target; + printf("%s kind=%s tracer=%s rdonly=%d vm=%d\n", name, kind, tracer == 0 ? "hidden" : tracer > 0 ? "shown" : "missing", + (fcntl(fd, F_GETFL) & O_ACCMODE) == O_RDONLY, strstr(text, "\nVmRSS:") != NULL); + close(fd); +} + +static void report_stream(const char *name, FILE *f) { + if (f == NULL) { + printf("%s failed\n", name); + return; + } + report(name, dup(fileno(f))); + fclose(f); +} + +int main(void) { + int ready[2]; + if (pipe(ready) != 0) return 1; + pid_t child = fork(); + if (child == 0) { + char ok = ptrace(PTRACE_TRACEME, 0, NULL, NULL) == 0; + if (write(ready[1], &ok, 1) != 1) _exit(1); + for (;;) pause(); + } + char ok = 0; + if (read(ready[0], &ok, 1) != 1 || !ok) { + kill(child, SIGKILL); + waitpid(child, NULL, 0); + puts("no ptrace"); + return 0; + } + char other[64], self[64]; + snprintf(other, sizeof(other), "/proc/%d/status", (int)child); + snprintf(self, sizeof(self), "/proc/%d/status", (int)getpid()); + report("main_other_open", open(other, O_RDONLY | O_CLOEXEC)); + report_stream("main_other_fopen", fopen(other, "r")); + report("main_self_open", open("/proc/self/status", O_RDONLY | O_CLOEXEC)); + report("ntdll_other_open", ntdll_open(other)); + report("ntdll_other_openat", ntdll_openat(other)); + report_stream("ntdll_other_fopen", ntdll_fopen(other)); + report_stream("ntdll_self_fopen", ntdll_fopen(self)); + kill(child, SIGKILL); + waitpid(child, NULL, 0); + return 0; +} +""" + + +class TracerPreloadTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + if not shutil.which("cc"): + raise unittest.SkipTest("no compiler") + cls.tmp = tempfile.TemporaryDirectory() + tmp = Path(cls.tmp.name) + (tmp / "ntdll.c").write_text(NTDLL) + (tmp / "prog.c").write_text(PROGRAM) + sources = sorted(str(p) for p in PRELOAD.glob("*.c")) + subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-Wall", "-pthread", "-o", str(tmp / "libblsession.so"), *sources, "-ldl"], + check=True) + subprocess.run(["cc", "-shared", "-fPIC", "-O2", "-o", str(tmp / "ntdll.so"), str(tmp / "ntdll.c")], check=True) + subprocess.run(["cc", "-O1", "-o", str(tmp / "prog"), str(tmp / "prog.c"), str(tmp / "ntdll.so"), "-Wl,-rpath," + str(tmp)], + check=True) + env = dict(os.environ, LD_PRELOAD=str(tmp / "libblsession.so")) + out = subprocess.run([str(tmp / "prog")], env=env, capture_output=True, text=True, timeout=60) + if out.returncode != 0: + raise AssertionError(out.stdout + out.stderr) + if "no ptrace" in out.stdout: + raise unittest.SkipTest("ptrace not allowed") + cls.lines = dict(line.split(" ", 1) for line in out.stdout.splitlines()) + + @classmethod + def tearDownClass(cls): + cls.tmp.cleanup() + + def test_other_programs_still_see_no_tracer(self): + self.assertEqual(self.lines["main_other_open"], "kind=copy tracer=hidden rdonly=1 vm=1") + self.assertEqual(self.lines["main_other_fopen"], "kind=copy tracer=hidden rdonly=1 vm=1") + self.assertEqual(self.lines["main_self_open"], "kind=memfd tracer=hidden rdonly=0 vm=1") + + def test_wine_reads_the_real_status_file(self): + for name in ("ntdll_other_open", "ntdll_other_openat", "ntdll_other_fopen"): + self.assertEqual(self.lines[name], "kind=proc tracer=shown rdonly=1 vm=1", name) + self.assertEqual(self.lines["ntdll_self_fopen"], "kind=proc tracer=hidden rdonly=1 vm=1") + + +if __name__ == "__main__": + unittest.main()