From 2cdb4d40d46974f5385dd3c875ec602e892a24a0 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 3 Nov 2026 09:36:50 +1300 Subject: [PATCH] fix(relinker): start a deferred module's deferred dependencies before it on Windows Since #12621 a Windows runtime module that another runtime module needs is deferred instead of started at boot, but loading a deferred module only started that module. A Unity plugin such as SPFXUnityPlugin that calls into a deferred module it needs (Il2CppUserAssemblies in Media/Modules) ran against code whose initializers had never run, and PPSA03743 crashed about 2.6 s in. A deferred module's initialization table now lists the deferred modules it depends on (names, after the initializer array, behind a magic number so older output reads as having none). DynamicLoader starts those first, transitively and once each through the existing per-module state, with the start arguments of the load that began it, then starts the module itself. The module start result stays the module's own. Boot still starts no deferred module. Linux is unchanged: its deferred modules still start only when loaded. --- core/libs/prx/libkernel/CMakeLists.txt | 14 +++++ .../libkernel/Module/src/DynamicLoader.cpp | 48 ++++++++++++++++- .../tests/GuestDeferredDependentFixture.S | 10 ++++ core/libs/tests/GuestDeferredModule.cpp | 46 ++++++++++++++++- .../libs/tests/GuestDeferredModuleFixture.cpp | 11 +++- .../domain/include/domain/GuestRuntime.hpp | 1 + .../src/windows/WindowsGuestModuleWriter.cpp | 7 +++ .../include/relinker/guest/GuestImage.hpp | 2 + .../relinker/src/guest/GuestModuleBuilder.cpp | 1 + .../relinker/tests/ConversionTests.cpp | 41 +++++++++++++++ .../tests/test_guest_deferred_dependencies.py | 51 ++++++++++++++++++- docs/dev/debt/relinker.md | 2 +- docs/user/USAGE.md | 2 +- 13 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 core/libs/tests/GuestDeferredDependentFixture.S diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f4b59fbaee..7dd4a430d2 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -340,6 +340,20 @@ if(BUILD_TESTING) target_link_libraries(guest_deferred_module_tests PRIVATE libkernel libc) configure_windows_unwind(guest_deferred_module_tests) add_dependencies(guest_deferred_module_tests guest_deferred_module_fixture) + add_library(guest_deferred_dependent_fixture SHARED + ${LIBS_INCLUDE_DIR}/tests/GuestDeferredModuleFixture.cpp + ${LIBS_INCLUDE_DIR}/tests/GuestDeferredDependentFixture.S) + target_include_directories(guest_deferred_dependent_fixture PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_definitions(guest_deferred_dependent_fixture PRIVATE GUEST_DEFERRED_DEPENDENT_FIXTURE=1) + target_link_options(guest_deferred_dependent_fixture PRIVATE -Wl,--image-base,0x39000000 -Wl,--disable-dynamicbase) + configure_windows_unwind(guest_deferred_dependent_fixture) + set_target_properties(guest_deferred_dependent_fixture PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_deferred_module_tests guest_deferred_dependent_fixture) + foreach(mode IN ITEMS dependency dependency-nested) + add_test(NAME guest_deferred_module_${mode} COMMAND guest_deferred_module_tests $ ${mode} $) + set_tests_properties(guest_deferred_module_${mode} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endforeach() foreach(mode IN ITEMS reload pinned parallel reentrant) add_test(NAME guest_deferred_module_${mode} COMMAND guest_deferred_module_tests $ ${mode}) set_tests_properties(guest_deferred_module_${mode} PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 250680c0cd..4a3179bfa8 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -258,14 +258,58 @@ static void InitializeLinuxGuestModule(void* native) { } #endif #ifdef _WIN32 +static std::mutex initializedLock; +static std::condition_variable initializedChanged; + +static void InitializeDeferredModule(HMODULE native); + +static std::vector DeferredDependencyNames(const std::uint32_t* table) { + constexpr auto initializing = std::numeric_limits::max(); + constexpr std::uint32_t dependenciesMagic = 0x44504541; + constexpr std::uint32_t maximumDependencies = 4096; + std::vector names; + const auto count = table[1]; + if (count == initializing) return names; + const auto* list = table + 2 + count; + if (list[0] != dependenciesMagic || list[1] == 0 || list[1] > maximumDependencies) return names; + const auto* name = reinterpret_cast(list + 2); + for (std::uint32_t index = 0; index < list[1]; ++index) { + names.emplace_back(name); + name += names.back().size() + 1; + } + return names; +} + static void InitializeDeferredModule(HMODULE native) { using Entry = int (APS5_VABI *)(std::size_t, const void*, void*); using Initializer = void (APS5_VABI *)(int, char**, char**); constexpr auto initializing = std::numeric_limits::max(); - static std::mutex initializedLock; - static std::condition_variable initializedChanged; + thread_local std::vector resolving; + if (std::find(resolving.begin(), resolving.end(), native) != resolving.end()) return; auto* table = reinterpret_cast(GetProcAddress(native, "__aps5_guest_initialize")); if (!table) return; + std::vector dependencies; + { + std::lock_guard lock(initializedLock); + dependencies = DeferredDependencyNames(table); + } + if (!dependencies.empty()) { + auto* pendingArgs = const_cast(static_cast(__aps5_get_pending_module_args_nid_no_patch())); + const auto initiating = *pendingArgs; + resolving.push_back(native); + try { + for (const auto& name : dependencies) { + if (const auto dependency = GetModuleHandleA(name.c_str()); dependency && dependency != native) InitializeDeferredModule(dependency); + *pendingArgs = initiating; + } + } catch (...) { + *pendingArgs = initiating; + resolving.pop_back(); + throw; + } + resolving.pop_back(); + __aps5_set_module_init_result_nid_no_patch(0); + } const auto owner = GetCurrentThreadId(); std::unique_lock lock(initializedLock); while (table[1] == initializing) { diff --git a/core/libs/tests/GuestDeferredDependentFixture.S b/core/libs/tests/GuestDeferredDependentFixture.S new file mode 100644 index 0000000000..b4a5d51cb9 --- /dev/null +++ b/core/libs/tests/GuestDeferredDependentFixture.S @@ -0,0 +1,10 @@ +.section .ginit,"dr" +.p2align 3 +.globl __aps5_guest_initialize +__aps5_guest_initialize: +.rva DeferredStart_nid_no_patch +.long 1 +.rva DeferredInitializerSlot_nid_no_patch +.long 0x44504541 +.long 1 +.asciz "guest_deferred_module_fixture.guest.prx" diff --git a/core/libs/tests/GuestDeferredModule.cpp b/core/libs/tests/GuestDeferredModule.cpp index f9e3089049..a443672ca3 100644 --- a/core/libs/tests/GuestDeferredModule.cpp +++ b/core/libs/tests/GuestDeferredModule.cpp @@ -6,6 +6,7 @@ #include #include #include +#include static void Require(bool value, const char* what) { if (!value) { @@ -58,8 +59,29 @@ static void APS5_VABI Reenter(const char* path) { Require(dlclose_nid_postfix(nested) == 0, "recursive module close"); } +static int NativeCounter(HMODULE module, const char* name) { + using Value = int (APS5_VABI*)(); + const auto function = reinterpret_cast(GetProcAddress(module, name)); + Require(function != nullptr, "find fixture counter"); + return function(); +} + +static void APS5_VABI ProviderStartedFirst(const char* path) { + const auto provider = GetModuleHandleW(std::filesystem::absolute(path).c_str()); + Require(provider != nullptr, "the provider is mapped"); + Require(NativeCounter(provider, "DeferredStarts_nid_no_patch") == 1 && NativeCounter(provider, "DeferredConstructors_nid_no_patch") == 1, + "the dependency was started before the module that needs it"); +} + +static void APS5_VABI NestedLoad(const char* path) { + int result = -1; + const int id = sceKernelLoadStartModule(path, 0, nullptr, 0, nullptr, &result); + Require(id >= 0, "nested load from a dependency"); + Require(sceKernelStopUnloadModule(id, 0, nullptr, 0, nullptr, nullptr) == 0, "nested close"); +} + int main(int argc, char** argv) { - Require(argc == 3, "fixture and mode required"); + Require(argc == 3 || (argc == 4 && std::string(argv[2]).starts_with("dependency")), "fixture and mode required"); const auto file = std::filesystem::absolute(argv[1]); const auto path = file.string(); const std::string mode = argv[2]; @@ -130,5 +152,27 @@ int main(int argc, char** argv) { Counts(id, 1, 1); MetadataProtected(id); Close(id); + } else if (mode == "dependency" || mode == "dependency-nested") { + const auto provider = std::filesystem::absolute(argv[3]); + const auto providerPath = provider.string(); + HMODULE mapped = LoadLibraryW(provider.c_str()); + Require(mapped != nullptr, "map the provider without starting it"); + Require(NativeCounter(mapped, "DeferredStarts_nid_no_patch") == 0, "the provider is not started by mapping it"); + GuestDeferredModuleArgs args; + args.Mode = mode == "dependency" ? 3 : 4; + args.Reenter = mode == "dependency" ? ProviderStartedFirst : NestedLoad; + args.Path = providerPath.c_str(); + const int dependent = Open(path, &args); + Counts(dependent, 1, 1); + Require(NativeCounter(mapped, "DeferredStarts_nid_no_patch") == 1 && NativeCounter(mapped, "DeferredConstructors_nid_no_patch") == 1, "the dependency started once"); + Require(NativeCounter(mapped, "DeferredLastArgumentBytes_nid_no_patch") == static_cast(sizeof(args)), "the dependency received the title's start arguments"); + const auto native = Native(dependent); + Require(NativeCounter(native, "DeferredLastArgumentBytes_nid_no_patch") == static_cast(sizeof(args)), "the module received its start arguments after its dependency"); + const int again = Open(providerPath, nullptr, false); + Counts(again, 1, 1); + MetadataProtected(again); + Close(again); + Close(dependent); + Require(FreeLibrary(mapped), "release the provider"); } else Require(false, "unknown fixture mode"); } diff --git a/core/libs/tests/GuestDeferredModuleFixture.cpp b/core/libs/tests/GuestDeferredModuleFixture.cpp index 8091cc4fc8..6cbe830a3b 100644 --- a/core/libs/tests/GuestDeferredModuleFixture.cpp +++ b/core/libs/tests/GuestDeferredModuleFixture.cpp @@ -3,18 +3,26 @@ #include #include +#ifdef GUEST_DEFERRED_DEPENDENT_FIXTURE +static constexpr bool Dependent = true; +#else +static constexpr bool Dependent = false; +#endif + static std::atomic starts{0}; +static std::atomic lastBytes{0}; static std::atomic constructors{0}; extern "C" int APS5_VABI DeferredStart_nid_no_patch(std::size_t bytes, const void* argument, void*) { ++starts; + lastBytes = bytes; if (bytes != 0) { if (bytes != sizeof(GuestDeferredModuleArgs) || !argument) std::abort(); const auto& args = *static_cast(argument); if (args.Mode == 1) { if (!SetEvent(args.Entered) || WaitForSingleObject(args.Release, 10000) != WAIT_OBJECT_0) std::abort(); - } else if (args.Mode == 2) args.Reenter(args.Path); + } else if (args.Mode == 2 || (args.Mode == 3 && Dependent) || (args.Mode == 4 && !Dependent)) args.Reenter(args.Path); } return GuestDeferredModuleResult; } @@ -30,3 +38,4 @@ Initializer DeferredInitializerSlot_nid_no_patch = DeferredConstructor_nid_no_pa extern "C" int APS5_VABI DeferredStarts_nid_no_patch() { return starts.load(); } extern "C" int APS5_VABI DeferredConstructors_nid_no_patch() { return constructors.load(); } +extern "C" int APS5_VABI DeferredLastArgumentBytes_nid_no_patch() { return static_cast(lastBytes.load()); } diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index d4828544a6..79a13a155f 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -30,6 +30,7 @@ struct GuestRuntime { std::vector UnmatchedImportModules; std::set ZeroReturnExports; bool DeferInitialization = false; + std::vector DeferredDependencies; }; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 7a3794848e..9170f03979 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -153,6 +153,13 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI Io::AppendU32(lifecycle.Data, guest.HasInit ? image.GetRva(guest.Init) : 0); Io::AppendU32(lifecycle.Data, CheckedRva(guest.InitArray.size())); for (const auto slot : guest.InitArray) Io::AppendU32(lifecycle.Data, image.GetRva(slot, 8)); + if (runtime.DeferredDependencies.size() > Relinker::GuestMaximumDeferredDependencies) + throw Domain::RelinkerException("Too many deferred guest dependencies: " + std::to_string(runtime.DeferredDependencies.size())); + if (!runtime.DeferredDependencies.empty()) { + Io::AppendU32(lifecycle.Data, Relinker::GuestDeferredDependenciesMagic); + Io::AppendU32(lifecycle.Data, CheckedRva(runtime.DeferredDependencies.size())); + for (const auto& name : runtime.DeferredDependencies) Io::AppendString(lifecycle.Data, name); + } if (!exports.emplace(Relinker::GuestInitializeExport, nextRva).second) throw Domain::RelinkerException("Duplicate guest export: " + std::string(Relinker::GuestInitializeExport)); nextRva = AlignRva(nextRva + lifecycle.Data.size()); sections.push_back(std::move(lifecycle)); diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 2d6b51a189..d85dfc5e54 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -19,6 +19,8 @@ inline constexpr char GuestModuleSuffix[] = ".guest.prx"; inline constexpr char GuestModulePattern[] = "sce_module/*.prx"; inline constexpr std::uint16_t AbsoluteSection = 0xfff1; inline constexpr char GuestInitializeExport[] = "__aps5_guest_initialize"; +inline constexpr std::uint32_t GuestDeferredDependenciesMagic = 0x44504541; +inline constexpr std::size_t GuestMaximumDeferredDependencies = 4096; struct GuestSymbol { std::string Name; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 4669660ff9..53b37b2a35 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -451,6 +451,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path runtime.Path = relativeDirectory + "/" + image.OutputName; runtime.Names = {Io::Utf8Path(image.SourcePath.filename()), image.Soname}; runtime.DeferInitialization = deferred(index); + if (runtime.DeferInitialization) for (const auto dependency : dependencies[index]) if (deferred(dependency)) runtime.DeferredDependencies.push_back(images[dependency].OutputName); std::vector output; const auto message = [&](std::string_view text) { progress.Message(text); }; if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime, message); diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 60a5bb4e85..33c03ed969 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include namespace { @@ -400,6 +402,41 @@ bool exports(const std::vector& pe, std::string_view name) { return false; } +bool zeroFilled(const std::vector& pe, std::uint32_t address) { + const auto header = Io::ReadU32(pe, 0x3c); + const auto sections = header + 24 + Io::ReadU16(pe, header + 20); + for (std::size_t index = 0; index < Io::ReadU16(pe, header + 6); ++index) { + const auto at = sections + index * 40; + const auto start = Io::ReadU32(pe, at + 12); + if (address >= start && address - start < std::max(Io::ReadU32(pe, at + 8), Io::ReadU32(pe, at + 16))) return Io::ReadU32(pe, at + 16) == 0; + } + throw std::runtime_error("Unmapped PE address"); +} + +std::vector deferredDependencies(const std::vector& pe) { + const auto directory = fileOffset(pe, Io::ReadU32(pe, Io::ReadU32(pe, 0x3c) + 24 + 112)); + const auto names = fileOffset(pe, Io::ReadU32(pe, directory + 32)); + const auto ordinals = fileOffset(pe, Io::ReadU32(pe, directory + 36)); + const auto functions = fileOffset(pe, Io::ReadU32(pe, directory + 28)); + for (std::size_t index = 0; index < Io::ReadU32(pe, directory + 24); ++index) { + if (std::string_view(reinterpret_cast(pe.data() + fileOffset(pe, Io::ReadU32(pe, names + index * 4)))) != Relinker::GuestInitializeExport) continue; + const auto address = Io::ReadU32(pe, functions + Io::ReadU16(pe, ordinals + index * 2) * 4); + std::vector result; + if (zeroFilled(pe, address)) return result; + const auto table = fileOffset(pe, address); + auto position = table + 8 + static_cast(Io::ReadU32(pe, table + 4)) * 4; + if (Io::ReadU32(pe, position) != Relinker::GuestDeferredDependenciesMagic) return result; + const auto count = Io::ReadU32(pe, position + 4); + position += 8; + for (std::uint32_t listed = 0; listed < count; ++listed) { + result.emplace_back(reinterpret_cast(pe.data() + position)); + position += result.back().size() + 1; + } + return result; + } + throw std::runtime_error("No deferred initialization table"); +} + void runtimeDirectories() { TestProgress progress; const Relinker::Conversion converter(progress); @@ -428,6 +465,10 @@ void runtimeDirectories() { for (const auto& module : output.Modules) { const bool plugin = module.SourcePath.filename() == "plugin.prx"; require(exports(module.Bytes, Relinker::GuestInitializeExport) == (plugin || std::string_view(directory) == "Media/Modules"), "Wrong start of a runtime module another runtime module needs"); + if (plugin) { + const auto listed = deferredDependencies(module.Bytes); + require(listed == (std::string_view(directory) == "Media/Modules" ? std::vector{"needed.prx.guest.prx"} : std::vector{}), "Wrong deferred dependencies of a plugin"); + } else if (exports(module.Bytes, Relinker::GuestInitializeExport)) require(deferredDependencies(module.Bytes).empty(), "A module without dependencies lists some"); } } } diff --git a/core/relinker/relinker/tests/test_guest_deferred_dependencies.py b/core/relinker/relinker/tests/test_guest_deferred_dependencies.py index 8143e4e4c5..db1cca4d16 100644 --- a/core/relinker/relinker/tests/test_guest_deferred_dependencies.py +++ b/core/relinker/relinker/tests/test_guest_deferred_dependencies.py @@ -6,7 +6,7 @@ import subprocess import sys import tempfile -from test_guest_intel_trampolines import main_fixture +from test_guest_intel_trampolines import main_fixture, pe_sections from test_guest_module_args import args_module, load_and_record from test_guest_module_dir import pe_export_names from test_guest_module_directories import module_with_symbol, needed_libraries @@ -29,6 +29,45 @@ def page_aligned(image): return aligned +def deferred_dependencies(data): + header, = struct.unpack_from("` at startup for each such module that an import of the executable names, whether or not the title loads it later; an import without a module name is not reported. macOS still starts it at startup. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index a7c6e808b8..41d1e10366 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -84,7 +84,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--rpath ` | Set the system library search path. Quote `$ORIGIN` to prevent shell expansion, for example `--rpath '$ORIGIN/libs'` in Bash or PowerShell. On Linux every colon-separated entry must be nonempty, and with guest modules each must be absolute or begin with `$ORIGIN`. Windows requires a nonempty ASCII path and supports `$ORIGIN` as the executable directory. | | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | -| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. Needed modules are mapped at startup. On Linux, modules in these extra directories initialize only when explicitly loaded, with the arguments from `sceKernelLoadStartModule` (for example Unity plugins in `Media/Plugins`). On Windows, such a module initializes when explicitly loaded unless a module outside these directories needs it, directly or through other modules; then it initializes at startup. On macOS, a module another module or the executable needs initializes at startup. Other modules initialize when loaded. Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | +| `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. Needed modules are mapped at startup. On Linux, modules in these extra directories initialize only when explicitly loaded, with the arguments from `sceKernelLoadStartModule` (for example Unity plugins in `Media/Plugins`). On Windows, such a module initializes when explicitly loaded, after the modules it needs that initialize the same way (they receive the same start arguments), unless a module outside these directories needs it, directly or through other modules; then it initializes at startup. On macOS, a module another module or the executable needs initializes at startup. Other modules initialize when loaded. Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | | `--executable ` | Also convert another executable of the title, which it starts with `sceSystemServiceLoadExec`; the path is relative to the module parent directory, for example `picker/picker.bin`. It is written beside the output, named after the path with `/` replaced by `.` and given the output's extension (`picker.picker.bin.exe`), and uses the same `libs/`, `app0/` and guest modules. It is an error if a guest module converts differently for it. Repeatable. Conflicts with `--skip-sce-module`. | | `--patches ` | Apply the byte patches of this file during conversion, see [game patches](#game-patches). Overrides `relinker-patches.toml` discovery. Conflicts with `--no-patches`. | | `--no-patches` | Do not apply patches, and do not load `relinker-patches.toml` from the input directory. |