perf: let Wine read process memory counters from /proc directly (#1374)

This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-10-23 07:18:56 -04:00
1 parent ffa40a15d5
commit ac0909eaa3
4 files changed
+188 -31

No files matched your search

+1 -1
View File
@@ -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-<id>.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/<pid>/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-<id>.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/<pid>/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/<pid>/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.
+4 -1
View File
@@ -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 (;;) {
+29 -29
View File
@@ -10,6 +10,34 @@
#include <ucontext.h>
#include <unistd.h>
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 <asm/hwcap.h>
#include <asm/sigcontext.h>
@@ -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);
}
+154
View File
@@ -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 <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
__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 <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <unistd.h>
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()