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:
parent
33e9a12159
commit
1d9d5ab491
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue