mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
fix: let Wine write hooks into code FEX has translated Wine 11's wineserver writes another process's memory with process_vm_writev, which the kernel refuses on a page that is not writable. FEX's WoW64 layer keeps the pages it has translated read-only to catch self-modifying code and re-protects them right after a program unprotects them, so WriteProcessMemory and NtWriteVirtualMemory into ntdll failed with a partial copy and Ubisoft games stopped at "Error at hooking API NtProtectVirtualMemory". Wine only falls back to /proc/<pid>/mem on ENOSYS or EPERM. libblsession now completes a single-buffer write that wineserver was refused with EFAULT through /proc/<pid>/mem, the way Wine wrote before Wine 11. Other programs and every other error keep the kernel's answer. The iovecs are read with process_vm_readv, so a bad pointer still returns EFAULT, and offsets are 64-bit in the i386 build.
109 lines
4.2 KiB
Python
109 lines
4.2 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"""
|
|
#define _GNU_SOURCE
|
|
#include <errno.h>
|
|
#include <stdint.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/prctl.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
int main(void) {
|
|
unsigned char *code = mmap((void *)0xc0000000UL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
if (sizeof(void *) == 4 && (uintptr_t)code <= 0x7fffffffU) return 3;
|
|
unsigned char *data = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
memset(code, 0x90, 4096);
|
|
mprotect(code, 4096, PROT_READ | PROT_EXEC);
|
|
int ready[2];
|
|
if (pipe(ready)) return 2;
|
|
pid_t child = fork();
|
|
if (child == 0) {
|
|
prctl(PR_SET_PDEATHSIG, SIGKILL);
|
|
close(ready[0]);
|
|
if (write(ready[1], "", 1) != 1) _exit(1);
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
char c;
|
|
close(ready[1]);
|
|
if (read(ready[0], &c, 1) != 1) return 2;
|
|
unsigned char patch[4] = {0xB8, 0x78, 0x56, 0x34};
|
|
struct iovec mine = {patch, sizeof patch};
|
|
struct iovec theirs[2] = {{code, sizeof patch}, {data, sizeof patch}};
|
|
for (int i = 0; i < 2; i++) {
|
|
ssize_t n = process_vm_writev(child, &mine, 1, &theirs[i], 1, 0);
|
|
printf("%s %zd %d", i ? "data" : "code", n, n < 0 ? errno : 0);
|
|
unsigned char back[4] = {0};
|
|
struct iovec got = {back, sizeof back};
|
|
process_vm_readv(child, &got, 1, &theirs[i], 1, 0);
|
|
printf(" %s\n", memcmp(back, patch, sizeof patch) ? "old" : "new");
|
|
}
|
|
struct iovec split[2] = {{patch, 2}, {patch + 2, 2}};
|
|
ssize_t n = process_vm_writev(child, split, 2, &theirs[0], 1, 0);
|
|
printf("split %zd %d\n", n, n < 0 ? errno : 0);
|
|
n = process_vm_writev(child, (const struct iovec *)8, 1, &theirs[0], 1, 0);
|
|
printf("invalid %zd %d\n", n, n < 0 ? errno : 0);
|
|
kill(child, SIGKILL);
|
|
waitpid(child, NULL, 0);
|
|
return 0;
|
|
}
|
|
"""
|
|
|
|
|
|
@unittest.skipUnless(shutil.which("cc") and os.path.exists("/proc/self/mem"), "needs cc and /proc")
|
|
class VmWritePreloadTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.tmp = Path(tempfile.mkdtemp())
|
|
cls.addClassCleanup(shutil.rmtree, cls.tmp, ignore_errors=True)
|
|
source = cls.tmp / "program.c"
|
|
source.write_text(PROGRAM)
|
|
cls.bits32 = True
|
|
for flags, sub in (([], "64"), (["-m32"], "32")):
|
|
(cls.tmp / sub).mkdir()
|
|
try:
|
|
subprocess.run(["cc", *flags, "-O2", "-Wall", "-Werror", "-fPIC", "-shared", "-o",
|
|
str(cls.tmp / sub / "libvmwrite.so"), str(PRELOAD / "vmwrite.c"), "-ldl"],
|
|
check=True, capture_output=True)
|
|
for name in ("wineserver", "game"):
|
|
subprocess.run(["cc", *flags, "-O2", "-o", str(cls.tmp / sub / name), str(source)],
|
|
check=True, capture_output=True)
|
|
except subprocess.CalledProcessError:
|
|
if not flags:
|
|
raise
|
|
cls.bits32 = False
|
|
|
|
def run_as(self, name, preload=True, bits="64"):
|
|
lib = self.tmp / bits / "libvmwrite.so"
|
|
env = dict(os.environ, LD_PRELOAD=str(lib)) if preload else {k: v for k, v in os.environ.items() if k != "LD_PRELOAD"}
|
|
result = subprocess.run([str(self.tmp / bits / name)], env=env, capture_output=True, text=True, check=True)
|
|
return result.stdout.splitlines()
|
|
|
|
def test_wineserver_writes_a_page_the_kernel_refused(self):
|
|
self.assertEqual(self.run_as("wineserver"), ["code 4 0 new", "data 4 0 new", "split -1 14", "invalid -1 14"])
|
|
|
|
def test_a_32_bit_wineserver_writes_above_2_gib(self):
|
|
if not self.bits32:
|
|
self.skipTest("needs cc -m32")
|
|
self.assertEqual(self.run_as("wineserver", bits="32"), ["code 4 0 new", "data 4 0 new", "split -1 14", "invalid -1 14"])
|
|
|
|
def test_everyone_else_keeps_the_kernel_answer(self):
|
|
for refused in (self.run_as("game"), self.run_as("wineserver", preload=False)):
|
|
self.assertEqual(refused, ["code -1 14 old", "data 4 0 new", "split -1 14", "invalid -1 14"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|