fix(libs): keep a $ORIGIN runpath in the patched prx libraries

The NID patcher rebuilds .dynstr keeping only the DT_NEEDED and
DT_SONAME strings, so DT_RUNPATH kept a stale offset and the patched
libraries shipped with an empty runpath. A library could then load only
those sibling prx that the executable already loads itself:
libSceVideoOut.prx needs libSceKeyboard.prx, which a title that does
not import libSceKeyboard never loads, so it fails at startup.

The patcher now keeps DT_RPATH and DT_RUNPATH strings, and the prx
libraries are linked with a $ORIGIN runpath instead of the build
directory, so they find each other wherever they are copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alfalfa-star 2026-10-05 20:20:14 +02:00
parent 33e9a12159
commit 1d9d5ab491
5 changed files with 60 additions and 2 deletions

View File

@ -51,6 +51,8 @@ set_target_properties(libc PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR}
BUILD_WITH_INSTALL_RPATH ON
INSTALL_RPATH "$ORIGIN"
)
set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx)
@ -101,6 +103,8 @@ foreach(LIB_DIR ${LIBS_DIRS})
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR}
BUILD_WITH_INSTALL_RPATH ON
INSTALL_RPATH "$ORIGIN"
)
set(UNPATCHED_FILE ${UNPATCHED_DIR}/${LIB_NAME}.prx)

View File

@ -83,7 +83,13 @@ struct GnuHashLayout {
constexpr std::int64_t kDtNeeded = 1;
constexpr std::int64_t kDtSoname = 14;
constexpr std::int64_t kDtRpath = 15;
constexpr std::int64_t kDtRunpath = 29;
constexpr std::int64_t kDtNull = 0;
inline bool IsDynamicNameTag(std::int64_t tag) {
return tag == kDtNeeded || tag == kDtSoname || tag == kDtRpath || tag == kDtRunpath;
}
constexpr std::uint32_t kShtDynamic = 6u;
constexpr std::uint32_t kShtGnuVerneed = 0x6ffffffeu;
constexpr std::uint32_t kShtDynsym = 11u;

View File

@ -190,7 +190,7 @@ void RemapDynamicNeededOffsets(
for (std::size_t d = 0u; d < dynCount; ++d) {
const std::size_t entryOffset = dynOffset + d * sizeof(Elf64_Dyn);
const auto dyn = Read<Elf64_Dyn>(elf, entryOffset);
if (dyn.d_tag != kDtNeeded && dyn.d_tag != kDtSoname) continue;
if (!IsDynamicNameTag(dyn.d_tag)) continue;
const auto oldNameOffset = static_cast<std::uint32_t>(dyn.d_val);
const auto mapped = std::find_if(
@ -266,7 +266,7 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
for (std::size_t d = 0u; d < dynCount; ++d) {
const auto dyn = Read<Elf64_Dyn>(elf, dynOffset + d * sizeof(Elf64_Dyn));
if (dyn.d_tag == kDtNull) break;
if (dyn.d_tag != kDtNeeded && dyn.d_tag != kDtSoname) continue;
if (!IsDynamicNameTag(dyn.d_tag)) continue;
offsets.push_back(static_cast<std::uint32_t>(dyn.d_val));
}
}

View File

@ -0,0 +1,42 @@
import re
import struct
import sys
from pathlib import Path
DT_NULL = 0
DT_NEEDED = 1
DT_SONAME = 14
DT_RPATH = 15
DT_RUNPATH = 29
SHT_DYNAMIC = 6
def dynamic_strings(image):
shoff, = struct.unpack_from("<Q", image, 0x28)
shentsize, shnum = struct.unpack_from("<HH", image, 0x3A)
sections = [struct.unpack_from("<IIQQQQIIQQ", image, shoff + index * shentsize) for index in range(shnum)]
dynamic = next(section for section in sections if section[1] == SHT_DYNAMIC)
strings = sections[dynamic[6]]
entries = []
for position in range(dynamic[4], dynamic[4] + dynamic[5], 16):
tag, value = struct.unpack_from("<qQ", image, position)
if tag == DT_NULL:
break
if tag in (DT_NEEDED, DT_SONAME, DT_RPATH, DT_RUNPATH):
assert value < strings[5], (tag, value)
start = strings[4] + value
entries.append((tag, image[start:image.index(0, start)].decode()))
return entries
def main():
entries = dynamic_strings(Path(sys.argv[1]).read_bytes())
runpaths = [value for tag, value in entries if tag in (DT_RPATH, DT_RUNPATH)]
assert runpaths == ["$ORIGIN"], entries
needed = [value for tag, value in entries if tag == DT_NEEDED]
assert needed and all(re.fullmatch(r"[\w.+-]+\.so(\.\d+)*", name) for name in needed), entries
print("NID patcher dynamic string tests passed")
if __name__ == "__main__":
main()

View File

@ -86,6 +86,12 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:nid_patcher> guest_module_fixture $<TARGET_FILE:guest_module_fixture>
VERBATIM)
add_dependencies(guest_module_fixture nid_patcher)
set_target_properties(guest_module_fixture PROPERTIES BUILD_WITH_INSTALL_RPATH ON INSTALL_RPATH "$ORIGIN")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND AND NOT WIN32)
add_test(NAME nid_patcher_dynamic_strings COMMAND ${Python3_EXECUTABLE}
${LIBS_INCLUDE_DIR}/nid/tests/test_dynamic_strings.py $<TARGET_FILE:guest_module_fixture>)
endif()
add_test_executable(guest_dynamic_loader_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoader.cpp)
target_include_directories(guest_dynamic_loader_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_dynamic_loader_tests PRIVATE libkernel libc)