From ec5c0dcd0cfcdd51cfe4056cfedb50b01ed998e5 Mon Sep 17 00:00:00 2001 From: RockENZO <125934469+RockENZO@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:33:31 +1030 Subject: [PATCH] fix(relinker): preserve executable exports in macOS output --- core/relinker/CMakeLists.txt | 3 + .../include/elfpatcher/macos/MacOsImage.hpp | 7 +- .../elfpatcher/src/macos/MacOsImage.cpp | 18 +- .../src/macos/MacOsMachOPatcher.cpp | 26 +++ .../tests/test_macos_executable_exports.py | 171 ++++++++++++++++++ 5 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 core/relinker/relinker/tests/test_macos_executable_exports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index c53be3987b..4c73d8cb0c 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -750,6 +750,9 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test(NAME macos_executable_exports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_executable_exports.py $) + set_tests_properties(macos_executable_exports PROPERTIES TIMEOUT 30 LABELS relinker) add_test(NAME macos_guest_weak_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_imports.py $) set_property(TEST macos_guest_weak_imports APPEND PROPERTY LABELS relinker) diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index 4f7ec0fee8..b65c2f4851 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -10,6 +10,9 @@ namespace Elfpatcher::MacOs { +inline constexpr std::uint64_t ExportKindAbsolute = 2; +inline constexpr std::uint64_t ExportWeakDefinition = 4; + struct MacOsBind { std::string Symbol; std::uint32_t Rva; @@ -19,7 +22,8 @@ struct MacOsBind { struct MacOsExport { std::string Symbol; - std::uint32_t Rva; + std::uint64_t Value; + std::uint64_t Flags = 0; }; struct MacOsTlsImport { @@ -32,6 +36,7 @@ struct MacOsTlsImport { struct MacOsTlsExport { std::string Symbol; std::uint64_t Offset; + std::uint64_t Flags = 0; }; struct MacOsImageInput { diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 838d0d6c8b..ae083e97a5 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -32,6 +32,7 @@ constexpr std::uint32_t MhDylib = 6; constexpr std::uint32_t MhNoUndefs = 0x1; constexpr std::uint32_t MhDyldLink = 0x4; constexpr std::uint32_t MhTwoLevel = 0x80; +constexpr std::uint32_t MhWeakDefines = 0x8000; constexpr std::uint32_t LcSegment64 = 0x19; constexpr std::uint32_t LcSymtab = 0x2; @@ -291,7 +292,7 @@ public: private: struct Node { std::vector>> Children; - std::optional Rva; + std::optional Export; std::uint64_t Offset = 0; }; @@ -323,8 +324,8 @@ private: rest = {}; } } - if (node->Rva) throw Domain::RelinkerException("Duplicate Mach-O export: " + entry.Symbol); - node->Rva = entry.Rva; + if (node->Export) throw Domain::RelinkerException("Duplicate Mach-O export: " + entry.Symbol); + node->Export = entry; } static void Collect(Node* node, std::vector& order) { @@ -334,9 +335,9 @@ private: static std::vector TerminalBytes(const Node& node) { std::vector bytes; - if (!node.Rva) return bytes; - AppendUleb(bytes, 0); - AppendUleb(bytes, *node.Rva); + if (!node.Export) return bytes; + AppendUleb(bytes, node.Export->Flags); + AppendUleb(bytes, node.Export->Value); return bytes; } @@ -443,7 +444,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { Io::WriteU64(meta, at, ImageBase + descriptor); Io::WriteU64(meta, at + 8, variable.Offset); input.Rebases.push_back(metaRva + static_cast(at)); - input.Exports.push_back({variable.Symbol, metaRva + static_cast(at)}); + input.Exports.push_back({variable.Symbol, metaRva + static_cast(at), variable.Flags}); } const auto codeRva = static_cast(AlignUp(metaRva + metaEnd, PageSize)); Windows::WindowsStubEmitter code(codeRva); @@ -804,7 +805,8 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { Io::WriteU32(file, 12, input.Executable ? MhExecute : MhDylib); Io::WriteU32(file, 16, commandCount); Io::WriteU32(file, 20, static_cast(commands.size())); - Io::WriteU32(file, 24, MhNoUndefs | MhDyldLink | MhTwoLevel); + const bool weakDefinitions = std::any_of(input.Exports.begin(), input.Exports.end(), [](const auto& entry) { return (entry.Flags & ExportWeakDefinition) != 0; }); + Io::WriteU32(file, 24, MhNoUndefs | MhDyldLink | MhTwoLevel | (weakDefinitions ? MhWeakDefines : 0)); std::copy(commands.begin(), commands.end(), file.begin() + HeaderSize); return file; } diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index f15b546181..0da4246eb0 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -65,6 +66,31 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou input.Source = &sourceElf; input.Headers = &originalHeaders; input.Image = ℑ + for (std::size_t offset = 24; offset < dynamicSection.DynSymData.size(); offset += 24) { + const auto& symbols = dynamicSection.DynSymData; + const auto section = Io::ReadU16(symbols, offset + 6); + const auto info = symbols[offset + 4]; + const auto visibility = symbols[offset + 5] & 3; + if (section == SHN_UNDEF || (info >> 4) == 0 || visibility == 1 || visibility == 2) continue; + if (((info >> 4) != STB_GLOBAL && (info >> 4) != STB_WEAK) || + ((info & 15) != STT_NOTYPE && (info & 15) != 1 && (info & 15) != STT_FUNC && (info & 15) != STT_TLS)) + throw Domain::RelinkerException("Unsupported macOS executable export attributes"); + const auto name = "_" + Windows::ReadString(dynamicSection.DynStrData, Io::ReadU32(symbols, offset)); + if (name.size() == 1) throw Domain::RelinkerException("Empty macOS executable export"); + const auto value = Io::ReadU64(symbols, offset + 8); + const auto flags = (info >> 4) == STB_WEAK ? ExportWeakDefinition : std::uint64_t{0}; + if ((info & 15) == STT_TLS) { + if (section >= SHN_LORESERVE) throw Domain::RelinkerException("Unsupported macOS executable TLS export section: " + name); + const auto tls = std::find_if(originalHeaders.begin(), originalHeaders.end(), [](const auto& header) { return header.Type == 7; }); + if (tls != originalHeaders.end() && (value > tls->MemorySize || Io::ReadU64(symbols, offset + 16) > tls->MemorySize - value)) + throw Domain::RelinkerException("macOS executable TLS export exceeds PT_TLS: " + name); + input.TlsExports.push_back({name, value, flags}); + } else if (section == SHN_ABS) input.Exports.push_back({name, value, flags | ExportKindAbsolute}); + else { + if (section >= SHN_LORESERVE) throw Domain::RelinkerException("Unsupported macOS executable export section: " + name); + input.Exports.push_back({name, image.GetRva(value, std::max(Io::ReadU64(symbols, offset + 16), 1)), flags}); + } + } input.Rebases = relocations.BaseRelocations; for (const auto& import : relocations.Imports) input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend), import.Weak}); diff --git a/core/relinker/relinker/tests/test_macos_executable_exports.py b/core/relinker/relinker/tests/test_macos_executable_exports.py new file mode 100644 index 0000000000..3a8640e806 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_executable_exports.py @@ -0,0 +1,171 @@ +from pathlib import Path +import os +import platform +import struct +import subprocess +import sys +import tempfile + +from test_linux_load_alignment import fixture +from test_macos_output import MachO + + +ABSOLUTE = 0x123456789ABCDEF +SYMBOLS = ( + ('FixtureExport#A#B', 0x12, 0, 1, 0x210, 6), + ('FixtureData#A#B', 0x11, 0, 1, 0x280, 4), + ('FixtureWeak#A#B', 0x22, 0, 1, 0x220, 6), + ('FixtureAbsolute#A#B', 0x11, 0, 0xFFF1, ABSOLUTE, 0), + ('FixtureHidden#A#B', 0x12, 2, 1, 0x210, 6), + ('FixtureLocal#A#B', 0x02, 0, 1, 0x210, 6), + ('FixtureProtected#A#B', 0x12, 3, 1, 0x210, 6), +) + + +def executable(symbols=SYMBOLS, tls=False): + image = fixture() + struct.pack_into('\n' + '#include \n' + '#include \n' + 'extern int FixtureExport(void);\n' + 'extern int FixtureData;\n' + 'void Aps5StartGuest_nid_no_patch(void) {\n' + ' int (*weak)(void) = dlsym(RTLD_DEFAULT, "FixtureWeak");\n' + ' int (*protected)(void) = dlsym(RTLD_DEFAULT, "FixtureProtected");\n' + ' if (!weak || weak() != 9 || !protected || protected() != 42) _exit(1);\n' + f' if ((uintptr_t)dlsym(RTLD_DEFAULT, "FixtureAbsolute") != {ABSOLUTE}ULL) _exit(2);\n' + ' if (dlsym(RTLD_DEFAULT, "FixtureHidden") || dlsym(RTLD_DEFAULT, "FixtureLocal")) _exit(3);\n' + ' _exit(FixtureExport() + FixtureData - 5);\n' + '}\n') + for name in ('libkernel.prx', 'libc.prx'): + subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', '-dynamiclib', str(source), + '-undefined', 'dynamic_lookup', '-Wl,-install_name,@rpath/' + name, + '-o', str(libraries / name)], check=True, capture_output=True, timeout=20) + source.write_text('int FixtureHostLibc(void) { return 0; }\n') + output.chmod(0o755) + command = (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(output)] + run = subprocess.run(command, capture_output=True, timeout=10) + if run.returncode != 42 and b'Bad CPU type in executable' in run.stderr: + assert os.environ.get('GITHUB_ACTIONS') != 'true', 'macOS CI must execute the x86_64 export probe' + return + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + print('macOS executable exports: native x86_64 imports and dlsym passed') + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-macos-executable-exports-') as directory: + work = Path(directory) + run, output = convert(relinker, work, 'exports', executable()) + assert run.returncode == 0, (run.stdout, run.stderr) + data = output.read_bytes() + entries = exports(data) + base = MachO(data).segment('__ELF0')['vmaddr'] + assert entries == { + '_FixtureExport': (0, base + 0x210 - 0x140000000), + '_FixtureData': (0, base + 0x280 - 0x140000000), + '_FixtureWeak': (4, base + 0x220 - 0x140000000), + '_FixtureAbsolute': (2, ABSOLUTE), + '_FixtureProtected': (0, base + 0x210 - 0x140000000), + }, entries + assert MachO(data).flags & 0x8000 + native_execution(work, output) + tls_symbols = (('FixtureTls#A#B', 0x16, 0, 1, 4, 4),) + for info, flags in ((0x16, 0), (0x26, 4)): + run, output = convert(relinker, work, f'tls-{info}', executable((('FixtureTls#A#B', info, 0, 1, 4, 4),), tls=True)) + assert run.returncode == 0, (run.stdout, run.stderr) + data = output.read_bytes() + entry, = exports(data).values() + assert entry[0] == flags + assert bool(MachO(data).flags & 0x8000) == bool(flags) + meta = MachO(data).segment('__APS5DATA') + slot = meta['fileoff'] + entry[1] + 0x140000000 - meta['vmaddr'] + assert struct.unpack_from('