fix(relinker): preserve GUI startup failure status without stderr

This commit is contained in:
Ethan Barr 2026-10-07 07:25:49 -07:00
parent cea1ef13c3
commit 2ed81aebe4
2 changed files with 96 additions and 19 deletions

View File

@ -162,33 +162,32 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
code.Emit({0x0f, 0x0b});
};
const auto requireDiagnosticSuccess = [&] {
code.Emit({0x48, 0x85, 0xc0});
const auto success = code.Branch({0x0f, 0x85});
raise(0xc0000001u);
code.PatchBranch(success, code.GetRva());
};
const auto writeString = [&](const std::uint32_t stringRva, const bool isError = false) {
code.Rip({0x48, 0x8d, 0x0d}, stringRva);
call("lstrlenA");
code.Emit({0x48, 0x85, 0xc0});
const auto empty = code.Branch({0x0f, 0x84});
code.Emit({0x89, 0x44, 0x24, 0x3c, 0xb9});
code.U32(isError ? 0xfffffff4u : 0xfffffff5u);
call("GetStdHandle");
requireDiagnosticSuccess();
code.Emit({0x48, 0x85, 0xc0});
const auto noHandle = code.Branch({0x0f, 0x84});
code.Emit({0x48, 0x83, 0xf8, 0xff});
const auto validHandle = code.Branch({0x0f, 0x85});
raise(0xc0000001u);
code.PatchBranch(validHandle, code.GetRva());
const auto invalidHandle = code.Branch({0x0f, 0x84});
code.Emit({0x48, 0x89, 0xc1});
code.Rip({0x48, 0x8d, 0x15}, stringRva);
code.Emit({0x44, 0x8b, 0x44, 0x24, 0x3c, 0x4c, 0x8d, 0x4c, 0x24, 0x38, 0x48, 0xc7, 0x44, 0x24, 0x20, 0, 0, 0, 0});
call("WriteFile");
requireDiagnosticSuccess();
code.Emit({0x48, 0x85, 0xc0});
const auto failedWrite = code.Branch({0x0f, 0x84});
code.Emit({0x8b, 0x44, 0x24, 0x38, 0x3b, 0x44, 0x24, 0x3c});
const auto complete = code.Branch({0x0f, 0x84});
raise(0xc0000001u);
code.PatchBranch(complete, code.GetRva());
const auto incompleteWrite = code.Branch({0x0f, 0x85});
const auto done = code.GetRva();
code.PatchBranch(empty, done);
code.PatchBranch(noHandle, done);
code.PatchBranch(invalidHandle, done);
code.PatchBranch(failedWrite, done);
code.PatchBranch(incompleteWrite, done);
};
const auto writeLastError = [&] {
@ -214,8 +213,10 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
code.U32(ErrorMessageCapacity);
code.Emit({0x48, 0xc7, 0x44, 0x24, 0x30, 0, 0, 0, 0});
call("FormatMessageA");
requireDiagnosticSuccess();
code.Emit({0x48, 0x85, 0xc0});
const auto noMessage = code.Branch({0x0f, 0x84});
writeString(errorMessage, true);
code.PatchBranch(noMessage, code.GetRva());
};
const auto captureLastError = [&] {

View File

@ -1,9 +1,11 @@
from pathlib import Path
import ctypes
import os
import struct
import subprocess
import sys
import tempfile
from ctypes import wintypes
from pathlib import Path
from test_guest_intel_trampolines import main_fixture
from test_guest_module_directories import module_with_symbol
@ -73,12 +75,72 @@ def executable(owner, symbol='shared#A#B', module_name=None, extra_dependencies=
return image
def shell_execute_exit_code(executable_path):
see_mask_nocloseprocess = 0x00000040
wait_object_0 = 0
wait_timeout = 0x00000102
class ShellExecuteInfo(ctypes.Structure):
_fields_ = [
('cbSize', wintypes.DWORD),
('fMask', wintypes.DWORD),
('hwnd', wintypes.HWND),
('lpVerb', wintypes.LPCWSTR),
('lpFile', wintypes.LPCWSTR),
('lpParameters', wintypes.LPCWSTR),
('lpDirectory', wintypes.LPCWSTR),
('nShow', ctypes.c_int),
('hInstApp', wintypes.HANDLE),
('lpIDList', wintypes.LPVOID),
('lpClass', wintypes.LPCWSTR),
('hkeyClass', wintypes.HANDLE),
('dwHotKey', wintypes.DWORD),
('hIconOrMonitor', wintypes.HANDLE),
('hProcess', wintypes.HANDLE),
]
shell32 = ctypes.WinDLL('shell32', use_last_error=True)
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
shell32.ShellExecuteExW.argtypes = [ctypes.POINTER(ShellExecuteInfo)]
shell32.ShellExecuteExW.restype = wintypes.BOOL
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
kernel32.WaitForSingleObject.restype = wintypes.DWORD
kernel32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
kernel32.GetExitCodeProcess.restype = wintypes.BOOL
kernel32.TerminateProcess.argtypes = [wintypes.HANDLE, wintypes.UINT]
kernel32.TerminateProcess.restype = wintypes.BOOL
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.CloseHandle.restype = wintypes.BOOL
info = ShellExecuteInfo()
info.cbSize = ctypes.sizeof(info)
info.fMask = see_mask_nocloseprocess
info.lpVerb = 'open'
info.lpFile = str(executable_path)
info.nShow = 1
if not shell32.ShellExecuteExW(ctypes.byref(info)):
raise OSError(ctypes.get_last_error(), 'ShellExecuteExW failed')
try:
result = kernel32.WaitForSingleObject(info.hProcess, 30_000)
if result == wait_timeout:
kernel32.TerminateProcess(info.hProcess, 0xffffffff)
kernel32.WaitForSingleObject(info.hProcess, 0xffffffff)
raise TimeoutError('ShellExecuteExW child did not exit')
if result != wait_object_0:
raise OSError(f'WaitForSingleObject returned {result}')
exit_code = wintypes.DWORD()
if not kernel32.GetExitCodeProcess(info.hProcess, ctypes.byref(exit_code)):
raise OSError(ctypes.get_last_error(), 'GetExitCodeProcess failed')
return exit_code.value
finally:
kernel32.CloseHandle(info.hProcess)
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix='anyps5-import-modules-') as directory:
work = Path(directory)
def convert(name, owner, symbol='shared#A#B', guest_owner=None, module_name=None, provider_name='b.prx', extra_dependencies=()):
def convert(name, owner, symbol='shared#A#B', guest_owner=None, module_name=None, provider_name='b.prx', extra_dependencies=(), windows_gui=False):
case = work / name
modules = case / 'prx'
modules.mkdir(parents=True)
@ -94,7 +156,11 @@ def main():
source = case / 'input.elf'
source.write_bytes(executable(owner, symbol, module_name, extra_dependencies))
output = case / 'output.exe'
result = subprocess.run([str(relinker), '--windows', str(source), str(output)], capture_output=True, text=True, timeout=30)
arguments = [str(relinker), '--windows']
if windows_gui:
arguments.append('--windows-gui')
arguments.extend([str(source), str(output)])
result = subprocess.run(arguments, capture_output=True, text=True, timeout=30)
return result, output
for owner in ('a.prx', 'b.prx', 'alias.prx'):
@ -143,6 +209,15 @@ def main():
assert result.returncode == 2 and 'Ambiguous guest import' in result.stderr, result.stderr
assert not output.exists()
if os.name == 'nt':
for name, owner, symbol, expected in [
('missing-symbol-gui', 'a.prx', 'absent#A#B', 0xc0000139),
('missing-dependency-gui', 'missing.prx', 'shared#A#B', 0xc0000135)]:
result, output = convert(name, owner, symbol, windows_gui=True)
assert result.returncode == 0, result.stderr
run_status = shell_execute_exit_code(output)
assert run_status == expected, (name, hex(run_status), hex(expected))
result, output = convert('missing-symbol', 'a.prx', 'absent#A#B')
assert result.returncode == 0, result.stderr
if os.name == 'nt':
@ -158,6 +233,7 @@ def main():
if os.name == 'nt':
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode != 0 and 'Failed to load module:' in run.stderr and 'missing.prx' in run.stderr, (run.returncode, run.stderr)
print('Windows module-scoped import tests passed')