https://bugs.winehq.org/show_bug.cgi?id=44893
Anastasius Focht focht@gmx.net changed:
What |Removed |Added ---------------------------------------------------------------------------- Status|UNCONFIRMED |NEW Component|-unknown |ntdll Keywords| |download, obfuscation Summary|SKSE64 fails to initialize |SKSE64 fails to initialize, |correctly |reports: "couldn't allocate | |trampoline, no free space | |before image" (virtual | |address space between | |KUSER_SHARED_DATA and main | |executable reported as | |reserved) Ever confirmed|0 |1 CC| |focht@gmx.net
--- Comment #5 from Anastasius Focht focht@gmx.net --- Hello folks,
confirming.
Download: http://skse.silverlock.org/download/archive/skse64_2_00_06.7z (to match 1.5.23.0 runtime).
Trace log:
--- snip --- $ pwd /home/focht/.wine/drive_c/Program Files (x86)/Bethesda Softworks/The Elder Scrolls V Skyrim - Special Edition
$ WINEDEBUG=+seh,+relay,+virtual,+module wine64 ./skse64_loader.exe >>log.txt 2>&1 ... 0036:Call KERNEL32.LoadLibraryA(000d6650 "C:\Program Files (x86)\Bethesda Softworks\The Elder Scrolls V Skyrim - Special Edition\\skse64_1_5_23.dll") ret=00b814b3 ... 0036:Call PE DLL (proc=0x615c4d74,module=0x61530000 L"skse64_1_5_23.dll",reason=PROCESS_ATTACH,res=(nil)) ... 0036:Call KERNEL32.CreateFileW(000db630 L"C:\users\focht\My Documents\My Games\Skyrim Special Edition\SKSE\skse64.log",40000000,00000001,0023e898,00000002,00000080,00000000) ret=615dafd3 0036:Ret KERNEL32.CreateFileW() retval=000000a8 ret=615dafd3 ... 0036:Call KERNEL32.GetModuleHandleA(00000000) ret=615c3062 0036:Ret KERNEL32.GetModuleHandleA() retval=140000000 ret=615c3062 0036:Call KERNEL32.VirtualQuery(13fffffff,0023f420,00000030) ret=615c3094 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 0036:Call KERNEL32.VirtualQuery(13fffefff,0023f420,00000030) ret=615c3094 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 0036:Call KERNEL32.VirtualQuery(13fffdfff,0023f420,00000030) ret=615c3094 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 0036:Call KERNEL32.VirtualQuery(13fffcfff,0023f420,00000030) ret=615c3094 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 0036:Call KERNEL32.VirtualQuery(13fffbfff,0023f420,00000030) ret=615c3094 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 ... 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 0036:Call KERNEL32.VirtualQuery(c8001fff,0023f420,00000030) ret=615c3094 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 0036:Call KERNEL32.VirtualQuery(c8000fff,0023f420,00000030) ret=615c3094 0036:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 0036:Call KERNEL32.GetLastError() ret=615d2c55 0036:Ret KERNEL32.GetLastError() retval=000000b7 ret=615d2c55 0036:Call KERNEL32.WriteFile(000000a8,0023ddd0,00000038,0023ddc0,00000000) ret=615d582c 0036:Ret KERNEL32.WriteFile() retval=00000001 ret=615d582c 0036:Call KERNEL32.WriteFile(000000a8,0023ddd0,00000002,0023ddc0,00000000) ret=615d582c 0036:Ret KERNEL32.WriteFile() retval=00000001 ret=615d582c 0036:Call KERNEL32.GetLastError() ret=615d2c55 0036:Ret KERNEL32.GetLastError() retval=000000b7 ret=615d2c55 0036:Call KERNEL32.GetLastError() ret=615d2c55 0036:Ret KERNEL32.GetLastError() retval=000000b7 ret=615d2c55 0036:Call KERNEL32.WriteFile(000000a8,0023de30,00000055,0023de20,00000000) ret=615d582c 0036:Ret KERNEL32.WriteFile() retval=00000001 ret=615d582c 0036:Call KERNEL32.WriteFile(000000a8,0023de30,00000002,0023de20,00000000) ret=615d582c 0036:Ret KERNEL32.WriteFile() retval=00000001 ret=615d582c 0036:Call KERNEL32.GetLastError() ret=615d2c55 0036:Ret KERNEL32.GetLastError() retval=000000b7 ret=615d2c55 0036:Ret PE DLL (proc=0x615c4d74,module=0x61530000 L"skse64_1_5_23.dll",reason=PROCESS_ATTACH,res=(nil)) retval=1 0036:trace:module:process_attach (L"skse64_1_5_23.dll",(nil)) - END ... --- snip ---
skse64.log:
--- snip --- SKSE64 runtime: initialize (version = 2.0.6 01050170 01D3F4E856187FD2, os = 6.1 (7601)) imagebase = 0000000140000000 reloc mgr imagebase = 0000000140000000 couldn't allocate trampoline, no free space before image couldn't create branch trampoline. this is fatal. skipping remainder of init process. --- snip ---
The are two problems, one being a Wine problem and the other one some SKSE64 brain damage that just works by chance on Windows due to OS loader behaviour.
'skse64_1_5_23.dll' has a preferred load base address of 0x180000000. 'skse64_1_5_23.dll' gets relocated to some lower 32-bit address 0x61530000 due to 'binkw64.dll' having the same fixed load base address (dll imports resolved before injection). NOTE: 'binkw64.dll' is not marked as relocatable.
--- snip --- Wine-dbg>info share Module Address Debug info Name (239 modules) PE 360000- 54c000 Deferred steam_api64 PE b80000- bb5000 Deferred skse64_steam_loader PE 61530000- 61665000 Deferred skse64_1_5_23 ELF 7a800000- 7aa9c000 Deferred opengl32<elf> -PE 7a8b0000- 7aa9c000 \ opengl32 ELF 7b400000- 7b882000 Dwarf kernel32<elf> -PE 7b460000- 7b882000 \ kernel32 ELF 7bc00000- 7bdbf000 Dwarf ntdll<elf> -PE 7bc80000- 7bdbf000 \ ntdll ELF 7c000000- 7c004000 Deferred <wine-loader> PE 140000000- 143726000 Export skyrimse PE 180000000- 180069000 Deferred binkw64 ELF 7f500d5ed000- 7f500d7f3000 Deferred libnss_dns.so.2 ELF 7f500d7f3000- 7f500d9f7000 Deferred libnss_mdns4_minimal.so.2 ELF 7f500d9f7000- 7f500dc09000 Deferred libnss_files.so.2 ELF 7f500dc09000- 7f500de12000 Deferred libffi.so.6 ... --- snip ---
Relevant disassembly snippet SKSE64 (annotated):
--- snip --- ... 0000000180093040 | mov qword ptr ss:[rsp + 10], rbx 000000018009304A | push rdi 000000018009304B | sub rsp, 50 000000018009304F | mov rax, r8 0000000180093052 | mov rbx, rcx 0000000180093055 | test r8, r8 0000000180093058 | jne skse64_1_5_23.180093062 000000018009305A | xor ecx, ecx 000000018009305C | call qword ptr ds:[<&GetModuleHandleA>] 0000000180093062 | cmp qword ptr ds:[rbx], 0 0000000180093066 | lea rsi, qword ptr ds:[rax - 78000000] ; low_end 000000018009306D | lea rdi, qword ptr ds:[rax - 1] ; low_start = start-1 0000000180093071 | jne skse64_1_5_23.18009315A 0000000180093077 | mov qword ptr ss:[rsp + 60], rbp 000000018009307C | xor ebp, ebp 000000018009307E | nop 0000000180093080 | mov r8d, 30 ; dwLength 0000000180093086 | lea rdx, qword ptr ss:[rsp + 20] ; lpBuffer 000000018009308B | mov rcx, rdi ; lpAddress 000000018009308E | call qword ptr ds:[<&VirtualQuery>] 0000000180093094 | test rax, rax 0000000180093097 | je skse64_1_5_23.18009313D 000000018009309D | cmp dword ptr ss:[rsp + 40], 10000 ; State == MEM_FREE 00000001800930A5 | jne skse64_1_5_23.18009310F 00000001800930A7 | mov rcx, qword ptr ss:[rsp + 38] ; RegionSize 00000001800930AC | cmp rcx, 10000 00000001800930B3 | jb skse64_1_5_23.18009310F 00000001800930B5 | lea rdi, qword ptr ds:[rcx - 10000] 00000001800930BC | mov edx, 10000 ; dwSize 00000001800930C1 | add rdi, qword ptr ss:[rsp + 20] ; Buffer.BaseAddress 00000001800930C6 | mov r9d, 40 ; flProtect 00000001800930CC | mov rcx, rdi ; lpAddress 00000001800930CF | mov r8d, 3000 ; flAllocationType 00000001800930D5 | call qword ptr ds:[<&VirtualAlloc>] 00000001800930DB | mov qword ptr ds:[rbx], rax 00000001800930DE | test rax, rax 00000001800930E1 | je skse64_1_5_23.1800930F1 00000001800930E3 | mov qword ptr ds:[rbx + 8], 10000 00000001800930EB | mov qword ptr ds:[rbx + 10], rbp 00000001800930EF | jmp skse64_1_5_23.18009310F 00000001800930F1 | call qword ptr ds:[<&GetLastError>] 00000001800930F7 | mov r8d, 10000 ; "trampoline alloc %016I64Xx%016I64X failed (%08X)" 00000001800930FD | lea rcx, qword ptr ds:[1800DCCD0] 0000000180093104 | mov r9d, eax 0000000180093107 | mov rdx, rdi 000000018009310A | call skse64_1_5_23.180004F10 ; LogMessage 000000018009310F | mov rcx, qword ptr ds:[rbx] 0000000180093112 | test rcx, rcx 0000000180093115 | jne skse64_1_5_23.18009311F 0000000180093117 | mov rdi, qword ptr ss:[rsp + 20] 000000018009311C | dec rdi 000000018009311F | cmp rdi, rsi 0000000180093122 | jb skse64_1_5_23.18009312F 0000000180093124 | test rcx, rcx 0000000180093127 | je skse64_1_5_23.180093080 000000018009312D | jmp skse64_1_5_23.180093151 ; "couldn't allocate trampoline, no free space before image" 000000018009312F | lea rcx, qword ptr ds:[1800DCD30] 0000000180093136 | call skse64_1_5_23.180004F10 ; LogMessage 000000018009313B | jmp skse64_1_5_23.180093151 000000018009313D | call qword ptr ds:[<&GetLastError>] 0000000180093143 | mov edx, eax ; "VirtualQuery failed: %08X" 0000000180093145 | lea rcx, qword ptr ds:[1800DCCB0] 000000018009314C | call skse64_1_5_23.180004F10 ; LogMessage 0000000180093151 | mov rbp, qword ptr ss:[rsp + 60] 0000000180093156 | cmp qword ptr ds:[rbx], 0 000000018009315A | mov rbx, qword ptr ss:[rsp + 68] 000000018009315F | setne al 0000000180093162 | mov rsi, qword ptr ss:[rsp + 70] 0000000180093167 | add rsp, 50 000000018009316B | pop rdi 000000018009316C | ret --- snip ---
The hooker searches for free memory regions directly below the main executable mapping (on 64-bit usually 0x140000000).
Address space layout on 64-bit target process:
--- snip --- Address Size Info ... 0000000061530000 0000000000001000 skse64_1_5_23.dll 0000000061531000 00000000000B6000 ".text" 00000000615E7000 0000000000057000 ".rdata" 000000006163E000 0000000000013000 ".data" 0000000061651000 000000000000B000 ".pdata", ".gfids" 000000006165C000 0000000000001000 ".tls" 000000006165D000 0000000000008000 ".rsrc", ".reloc" 0000000061670000 0000000000030000 00000000616A0000 0000000003FD0000 Reserved (0000000061670000) 0000000065670000 0000000000400000 0000000065A70000 0000000000001000 0000000065A71000 0000000000001000 0000000065A72000 00000000000FE000 Thread 37 Stack 0000000065B70000 0000000000004000 0000000065B80000 0000000000001000 0000000065B81000 0000000000001000 0000000065B82000 00000000000FE000 Thread 38 Stack 0000000065C80000 0000000000004000 0000000065C90000 0000000000001000 0000000065C91000 0000000000001000 0000000065C92000 00000000000FE000 Thread 39 Stack 0000000065D90000 0000000000004000 0000000065DA0000 0000000000001000 0000000065DA1000 0000000000001000 0000000065DA2000 00000000000FE000 Thread 3A Stack 0000000065EA0000 0000000000004000 0000000065EB0000 0000000000001000 0000000065EB1000 0000000000001000 0000000065EB2000 00000000000FE000 Thread 3B Stack 0000000065FB0000 0000000000004000 0000000065FC0000 0000000000001000 0000000065FC1000 0000000000001000 0000000065FC2000 00000000000FE000 Thread 3C Stack 00000000660C0000 0000000000004000 00000000660D0000 0000000000001000 00000000660D1000 0000000000001000 00000000660D2000 00000000000FE000 Thread 3D Stack 00000000661D0000 0000000000004000 00000000661E0000 0000000000001000 00000000661E1000 0000000000001000 00000000661E2000 00000000000FE000 Thread 3E Stack 00000000662E0000 0000000000004000 00000000662E4000 000000000020E000 0000000066500000 0000000000001000 0000000066501000 0000000000001000 0000000066502000 00000000000FE000 Thread 50 Stack 0000000066600000 0000000000004000 0000000066610000 0000000000001000 0000000066611000 0000000000001000 0000000066612000 00000000001FE000 Thread 51 Stack 0000000066810000 0000000000004000 0000000066920000 0000000000001000 0000000066921000 0000000000001000 0000000066922000 00000000001FE000 Thread 52 Stack 0000000066B20000 0000000000004000 0000000066B30000 0000000000001000 0000000066B31000 0000000000001000 0000000066B32000 00000000001FE000 Thread 53 Stack 0000000066D30000 0000000000004000 0000000066D40000 0000000000001000 0000000066D41000 0000000000001000 0000000066D42000 00000000001FE000 Thread 54 Stack 0000000066F40000 0000000000004000 0000000066F50000 0000000000001000 0000000066F51000 0000000000001000 0000000066F52000 00000000001FE000 Thread 55 Stack 0000000067150000 0000000000004000 0000000067160000 0000000000001000 0000000067161000 0000000000001000 0000000067162000 00000000001FE000 Thread 56 Stack 0000000067360000 0000000000004000 0000000067494000 00000000003E9000 000000006787D000 00000000001F4000 0000000067A71000 0000000000101000 0000000067B72000 0000000000081000 0000000067C23000 0000000000081000 0000000067CB0000 00000000001FB000 0000000067EB0000 0000000000110000 0000000067FC0000 0000000000001000 000000007A8B0000 0000000000001000 opengl32.dll 000000007A8B1000 00000000001DC000 ".text", ".reloc", ".rsrc" 000000007AA8D000 0000000000002000 opengl32.dll 000000007AA8F000 0000000000001000 opengl32.dll 000000007AA90000 000000000000C000 opengl32.dll 000000007B460000 0000000000001000 kernel32.dll 000000007B461000 000000000026F000 ".text", ".reloc", ".rsrc" 000000007B6D0000 000000000000B000 kernel32.dll 000000007B6DB000 0000000000001000 kernel32.dll 000000007B6DC000 00000000001A6000 kernel32.dll 000000007BC80000 0000000000001000 ntdll.dll 000000007BC81000 000000000011F000 ".text", ".reloc", ".rsrc" 000000007BDA0000 0000000000001000 ntdll.dll 000000007BDA1000 000000000001E000 ntdll.dll 000000007BDBF000 0000000004141000 Reserved 000000007FF10000 0000000000001000 000000007FF20000 0000000000001000 000000007FF30000 0000000000001000 000000007FF40000 0000000000001000 000000007FF50000 0000000000001000 000000007FF51000 0000000000081000 000000007FFE0000 0000000000010000 KUSER_SHARED_DATA 000000007FFF0000 00000000C0010000 Reserved 0000000140000000 0000000000001000 skyrimse.exe 0000000140001000 0000000001521000 ".text" 0000000141522000 00000000008B3000 ".rdata" 0000000141DD5000 00000000016E8000 ".data" 00000001434BD000 0000000000159000 ".pdata" 0000000143616000 0000000000005000 ".tls", ".text" 000000014361B000 00000000000E6000 ".gfids", ".rsrc", ".reloc" 0000000143701000 0000000000025000 ".bind" 0000000180000000 0000000000001000 binkw64.dll ... --- snip ---
As said, it searches top-down until it finds a free 64KB chunk between [image_load_base-1...image_load_base-0x78000000]. Starting with 4K chunk size, increasing in steps. The search range value is hard-coded.
Unfortunately Wine treats the virtual address space for 64-bit between default executable mapping (0x140000000) and KUSER_SHARED_DATA (0x7FFE0000) as reserved but not committed memory hence the hooker fails to find a free chunk.
https://source.winehq.org/git/wine.git/blob/HEAD:/dlls/ntdll/virtual.c#l2747
--- snip --- 2747 /* retrieve state for a free memory area; callback for wine_mmap_enum_reserved_areas */ 2748 static int get_free_mem_state_callback( void *start, size_t size, void *arg ) 2749 { 2750 MEMORY_BASIC_INFORMATION *info = arg; 2751 void *end = (char *)start + size; 2752 2753 if ((char *)info->BaseAddress + info->RegionSize < (char *)start) return 0; 2754 2755 if (info->BaseAddress >= end) 2756 { 2757 if (info->AllocationBase < end) info->AllocationBase = end; 2758 return 0; 2759 } 2760 2761 if (info->BaseAddress >= start || start <= address_space_start) 2762 { 2763 /* it's a real free area */ 2764 info->State = MEM_FREE; 2765 info->Protect = PAGE_NOACCESS; 2766 info->AllocationBase = 0; 2767 info->AllocationProtect = 0; 2768 info->Type = 0; 2769 if ((char *)info->BaseAddress + info->RegionSize > (char *)end) 2770 info->RegionSize = (char *)end - (char *)info->BaseAddress; 2771 } 2772 else /* outside of the reserved area, pretend it's allocated */ 2773 { 2774 info->RegionSize = (char *)start - (char *)info->BaseAddress; 2775 info->State = MEM_RESERVE; 2776 info->Protect = PAGE_NOACCESS; 2777 info->AllocationProtect = PAGE_NOACCESS; 2778 info->Type = MEM_PRIVATE; 2779 } 2780 return 1; 2781 } --- snip ---
I've modified the code the treat the address space above KUSER_SHARED_DATA also as "really free (tm)" and it allowed the hooker to continue.
Unfortunately the hooker has a fatal flaw. When it tries to allocate memory for the codegen buffer, it uses the same piece of code but now with the own module base as top-down search start.
NOTE: Trace done with patched Wine.
--- snip --- ... 002e:Call KERNEL32.VirtualQuery(13fff0fff,0023f420,00000030) ret=615c3094 002e:trace:virtual:get_free_mem_state_callback start=0x140000000, info->BaseAddress=0x13fff0000, address_space_start=0x10000 002e:trace:virtual:NtQueryVirtualMemory info->BaseAddress=0x13fff0000, info->RegionSize=0x10000, info->State=0x10000 002e:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 002e:Call KERNEL32.VirtualAlloc(13fff0000,00010000,00003000,00000040) ret=615c30db 002e:trace:virtual:NtAllocateVirtualMemory 0xffffffffffffffff 0x13fff0000 00010000 3000 00000040 002e:trace:virtual:VIRTUAL_DumpView View: 0x13fff0000 - 0x13fffffff (valloc) 002e:trace:virtual:VIRTUAL_DumpView 0x13fff0000 - 0x13fffffff c-rwx 002e:Ret KERNEL32.VirtualAlloc() retval=13fff0000 ret=615c30db 002e:Call KERNEL32.VirtualQuery(6152ffff,0023f420,00000030) ret=615c3094 002e:trace:virtual:get_free_mem_state_callback start=0x10000, info->BaseAddress=0x6152f000, address_space_start=0x10000 002e:trace:virtual:NtQueryVirtualMemory info->BaseAddress=0x6152f000, info->RegionSize=0x1000, info->State=0x10000 002e:Ret KERNEL32.VirtualQuery() retval=00000030 ret=615c3094 002e:Call KERNEL32.GetLastError() ret=615d2c55 002e:Ret KERNEL32.GetLastError() retval=000000b7 ret=615d2c55 ... --- snip ---
If the chunk is marked as free, the hooker checks the region size and goes in next iteration for larger chunk until it finds 64 KB free (after checking search range). Unfortunately there is an underflow in the range comparison logic (hard-coded 0x78000000). If the 64-bit hooker dll is mapped to low 32-bit range, the code can't work by design. On Windows, the hooker dll likely relocated to some > 32-bit address space range, hence the problem never occurs.
Anyway, there is still one valid problem in Wine - even if the hooker is kinda broken.
$ sha1sum skse64_2_00_06.7z c1467dd97dac9046fadb6f7b18ab68efea528b21 skse64_2_00_06.7z
$ du -sh skse64_2_00_06.7z 732K skse64_2_00_06.7z
$ wine --version wine-3.9
Regards