From f2b7333ae28c4b36613fa094ce9a18fa17eb31ef Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Thu, 29 Oct 2026 10:04:37 +0530 Subject: [PATCH] fix(relinker): make kernel-family fallback siblings optional on Windows Family fallback entries appended for kernel-variant imports were marked imported, so the startup stub hard-required every family member; a partial libs/ without the unused sibling aborted with a load failure. Track fallback positions separately so only the declared module and canonical libkernel.prx stay required while siblings probe and skip. Adds missing-sibling cases pinning expected providers (11 for the declared variant, 22 for canonical libkernel.prx) and trims the debt entry below the 1000-character limit. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/windows/WindowsEntryStubBuilder.cpp | 16 ++++++++++++---- .../tests/test_windows_kernel_variant_imports.py | 4 ++++ docs/dev/debt/relinker.md | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 413b689cea..b4d574cb23 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -166,6 +166,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector searchCounts; std::vector platformTlsResolvers; std::vector imported(libraries.size()); + std::vector familyFallbacks(libraries.size()); std::vector executableImports(guestModules.size()); const bool unscopedImports = std::any_of(imports.begin(), imports.end(), [](const auto& import) { return import.Library.empty(); }); for (const auto& import : imports) { @@ -182,12 +183,16 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con if (order.empty()) throw Domain::RelinkerException("Import module is not a dependency: " + import.Library); const bool hostModule = !import.Library.empty() && std::any_of(order.begin(), order.end(), [&](auto index) { return index >= guestModules.size(); }); + std::size_t familyBegin = order.size(); + std::size_t familyEnd = order.size(); if (hostModule) { if (library == "libkernel.prx" || library == "libkernel_sys.prx" || library == "libkernel_web.prx") { + familyBegin = order.size(); for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { if ((libraries[index] == "libkernel.prx" || libraries[index] == "libkernel_sys.prx" || libraries[index] == "libkernel_web.prx") && libraries[index] != library) order.push_back(CheckedRva(index)); } + familyEnd = order.size(); } for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { if (libraries[index] == "libc.prx" && library != "libc.prx") order.push_back(CheckedRva(index)); @@ -206,9 +211,12 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con })); searchTables.push_back(CheckedRva(dataRva + data.size())); searchCounts.push_back(CheckedRva(order.size())); - for (const auto index : order) { + for (std::size_t position = 0; position < order.size(); ++position) { + const auto index = order[position]; Io::AppendU32(data, index); - if (!import.Library.empty()) imported[index] = true; + const bool fallback = position >= familyBegin && position < familyEnd && libraries[index] != "libkernel.prx"; + if (fallback) familyFallbacks[index] = true; + else if (!import.Library.empty()) imported[index] = true; } } @@ -597,7 +605,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.PatchBranch(noAllocator, code.GetRva()); for (std::size_t index = 0; index < libraries.size(); ++index) { - if (index >= guestModules.size() && !imported[index] && !unscopedImports) continue; + if (index >= guestModules.size() && !imported[index] && !familyFallbacks[index] && !unscopedImports) continue; if (absolutePath && index >= guestModules.size()) { code.Rip({0x48, 0x8d, 0x0d}, libraryPaths[index]); } else { @@ -854,7 +862,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con code.PatchBranch(branch, code.GetRva()); writeString(searched, true); for (std::size_t index = 0; index < resolvedPaths.size(); ++index) { - if (index >= guestModules.size() && !imported[index] && !unscopedImports) continue; + if (index >= guestModules.size() && !imported[index] && !familyFallbacks[index] && !unscopedImports) continue; writeString(indent, true); writeString(resolvedPaths[index], true); writeString(newline, true); diff --git a/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py b/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py index d2fc1d0b32..7014b50c7b 100644 --- a/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py +++ b/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py @@ -77,12 +77,16 @@ def main(): ('variant-requests-other', variant, 'empty', 'empty', 'guest', 44), ('kernel-prefers-declared', 'libkernel.prx', 'variant', 'canonical', 'guest', 22), ('kernel-declared-siblings', 'libkernel.prx', 'variant', 'empty', 'canonical', sibling_expected), + ('variant-missing-sibling', variant, 'variant', 'canonical', None, 11), + ('kernel-missing-sibling', 'libkernel.prx', 'variant', 'canonical', None, 22), ): case = work / label libraries = case / 'libs' libraries.mkdir(parents=True) (case / 'prx').mkdir() for name, image in ((variant, variant_image), ('libkernel.prx', canonical_image), (other, other_image), ('libc.prx', 'empty')): + if image is None: + continue (libraries / name).write_bytes(images[image]) source = case / 'input.elf' source.write_bytes(executable(owner, 'shared#A#B')) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 169c3ca49a..d2fac4fc63 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -22,7 +22,7 @@ - An RSP operand that `--frame-red-zone` cannot move in place, an 8-bit displacement the shift takes past 0x7f or one without a displacement, moves to a stub with the instructions after it up to five bytes and is re-encoded there with the moved displacement ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). The leaf is skipped when that site would take in a branch target, a branch, ret or RIP-relative instruction, or another site, and when the operand is EVEX, whose 8-bit displacement is scaled. A game patch over a framed site or a moved displacement is refused. - In `--frame-red-zone`, function bounds come from the executable's `.eh_frame` FDEs: each function is decoded from its FDE start to its FDE end, or to int3 padding before it, and the relinker stops with an error when the executable has none. Code outside every FDE, such as a function without unwind information, is not framed ([framer](../../../core/relinker/codegen/src/x86/RedZoneFramer.cpp)). - `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified. -- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and every kernel-family module (`libkernel`, `libkernel_sys`, `libkernel_web`) also loads the other two and `libc.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. +- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and a kernel-family import searches the other members and `libc.prx`, undeclared variants optional, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. macOS resolves these imports through the host loader by name and is unchanged. - [Unscoped imports on Linux](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) - in an executable or bundled module that names a dependency with `.sprx`, `.suprx` or `.debug_prx`, as homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) does, an import without a module that is named by symbol is bound to the NID of that name only. Unlike Windows, the name as written is not tried first, because the host loader also searches the host C library, which exports `malloc` or `pthread_create` under their names. A name that a bundled module exports still binds to that module, as does a bundled module's import of a name the executable exports, and such a dependency loads the `.prx` library of the same stem. Unqualified imports of other executables and bundled modules are still bound by name. - An `R_X86_64_GLOB_DAT` or `R_X86_64_JUMP_SLOT` relocation with a nonzero addend fails the conversion in the Linux output ([ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp)), in the Windows and macOS outputs ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)) and in guest modules ([guest image reader](../../../core/relinker/relinker/src/guest/GuestImageReader.cpp)). What the console's loader does with such an addend is not measured. The x86-64 psABI computes both as S, but loaders differ: FreeBSD rtld uses S for GLOB_DAT and lazy JUMP_SLOT and S+A for eager JUMP_SLOT, and glibc used S+A for eager binding before [f8587a61892c](https://github.com/bminor/glibc/commit/f8587a61892cbafd98ce599131bf4f103466f084) (2.36) and S since. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. The rewriter conservatively rejects length-changing edits when a surviving truncated relative branch would move relative to its target (only one of the two lies after the cut) or when its section-relative target falls outside the 16-bit range, because the runtime address base needed to resolve a wrapped destination is unavailable; truncated branches that keep their distance and stay in range are accepted.