Repository navigation
Port open upstream relinker fixes (#4340, #4802, #4501) #439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fd6f716
fedfdf8
6e4ea3e
4f84665
2e8237b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| #include <filesystem> | ||
| #include <iostream> | ||
| #include <memory> | ||
| #include <random> | ||
| #include <string> | ||
| #include <vector> | ||
|
|
||
|
|
@@ -38,6 +39,100 @@ | |
|
|
||
| namespace { | ||
|
|
||
| #ifdef _WIN32 | ||
|
|
||
| class OutputIdentityDirectory { | ||
| public: | ||
| OutputIdentityDirectory(const OutputIdentityDirectory&) = delete; | ||
| OutputIdentityDirectory& operator=(const OutputIdentityDirectory&) = delete; | ||
|
|
||
| explicit OutputIdentityDirectory(const std::filesystem::path& parent) { | ||
| std::random_device random; | ||
| for (unsigned attempt = 0; attempt < 64; ++attempt) { | ||
| const auto candidate = parent / (".anyps5-output-identity-" + std::to_string(random()) + "-" + std::to_string(random())); | ||
| std::error_code error; | ||
| if (std::filesystem::create_directory(candidate, error)) { | ||
| directory = candidate; | ||
| return; | ||
| } | ||
| if (error && error != std::errc::file_exists) | ||
| throw std::filesystem::filesystem_error("Cannot create output identity directory", candidate, error); | ||
| } | ||
| throw Domain::RelinkerException("Cannot reserve an output identity directory in " + Io::Utf8Path(parent)); | ||
| } | ||
|
|
||
| ~OutputIdentityDirectory() { | ||
| if (directory.empty()) return; | ||
| try { | ||
| Remove(); | ||
| } catch (const std::exception& error) { | ||
| std::cerr << "FAIL: Output identity cleanup: " << error.what() << '\n'; | ||
| } | ||
| } | ||
|
|
||
| std::filesystem::path GetPath() const { return directory / "file"; } | ||
| const std::filesystem::path& GetDirectory() const { return directory; } | ||
|
|
||
| void Remove() { | ||
| std::filesystem::remove(GetPath()); | ||
| std::filesystem::remove(directory); | ||
| directory.clear(); | ||
| } | ||
|
|
||
| private: | ||
| std::filesystem::path directory; | ||
| }; | ||
|
|
||
| #endif | ||
|
|
||
| bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { | ||
| if (std::filesystem::equivalent(first, second)) return true; | ||
| #ifdef _WIN32 | ||
| if (std::filesystem::status(first).permissions() != std::filesystem::status(second).permissions() && | ||
| std::filesystem::hard_link_count(first) > 1 && std::filesystem::hard_link_count(second) > 1) { | ||
| for (const auto& path : {first, second}) { | ||
| if ((std::filesystem::status(path).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none) | ||
| throw Domain::RelinkerException("Cannot compare read-only output identities: " + Io::Utf8Path(path)); | ||
| } | ||
| OutputIdentityDirectory firstDirectory(first.parent_path()); | ||
| OutputIdentityDirectory secondDirectory(second.parent_path()); | ||
| std::filesystem::create_hard_link(first, firstDirectory.GetPath()); | ||
| std::filesystem::create_hard_link(second, secondDirectory.GetPath()); | ||
| const bool same = std::filesystem::equivalent(firstDirectory.GetPath(), secondDirectory.GetPath()); | ||
| secondDirectory.Remove(); | ||
| firstDirectory.Remove(); | ||
| return same; | ||
| } | ||
| #endif | ||
| return false; | ||
| } | ||
|
|
||
| bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { | ||
| const auto firstPath = std::filesystem::weakly_canonical(first); | ||
| const auto secondPath = std::filesystem::weakly_canonical(second); | ||
| if (firstPath == secondPath) return true; | ||
| if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); | ||
| #ifdef _WIN32 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On a case-insensitive POSIX filesystem, such as the default macOS APFS configuration, two nonexistent output paths that differ only by case remain unequal after Useful? React with 👍 / 👎. |
||
| auto firstName = Io::Utf8Path(firstPath.filename()); | ||
| auto secondName = Io::Utf8Path(secondPath.filename()); | ||
| for (auto* name : {&firstName, &secondName}) | ||
| for (auto& character : *name) | ||
| if (character >= 'A' && character <= 'Z') character = static_cast<char>(character + ('a' - 'A')); | ||
| if (firstName == secondName && std::filesystem::exists(firstPath.parent_path()) && | ||
| std::filesystem::exists(secondPath.parent_path()) && std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { | ||
| OutputIdentityDirectory probe(firstPath.parent_path()); | ||
| auto alternateName = Io::Utf8Path(probe.GetDirectory().filename()); | ||
| for (auto& character : alternateName) | ||
| if (character >= 'a' && character <= 'z') character = static_cast<char>(character - ('a' - 'A')); | ||
| const auto alternate = firstPath.parent_path() / Io::NativePath(alternateName); | ||
| const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(probe.GetDirectory(), alternate); | ||
| probe.Remove(); | ||
| return same; | ||
| } | ||
| #endif | ||
| return false; | ||
| } | ||
|
|
||
| int Run(const int argc, char* argv[]) { | ||
| Cli::Args args; | ||
| try { | ||
|
|
@@ -140,6 +235,23 @@ int Run(const int argc, char* argv[]) { | |
| for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); | ||
| } | ||
|
|
||
| std::vector<std::filesystem::path> outputPaths{Io::NativePath(absPath)}; | ||
| for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); | ||
| outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); | ||
| if (desktopEntry) { | ||
| outputPaths.push_back(desktopEntry->IconPath); | ||
| outputPaths.push_back(desktopEntry->EntryPath); | ||
| } | ||
| if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); | ||
| for (std::size_t index = 0; index < outputPaths.size(); ++index) { | ||
| for (std::size_t previous = 0; previous < index; ++previous) { | ||
| const auto& first = outputPaths[index]; | ||
| const auto& second = outputPaths[previous]; | ||
| if (SameOutputDestination(first, second)) | ||
| throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); | ||
| } | ||
| } | ||
|
|
||
| auto byteWriter = std::make_shared<Io::ByteWriter>(); | ||
|
|
||
| std::shared_ptr<Elfpatcher::IElfPatcher> patcher; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| from pathlib import Path | ||
| import os | ||
| import struct | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
|
|
||
| from test_gnu_hash_analysis import name_hash | ||
| from test_guest_intel_trampolines import main_fixture | ||
| from test_guest_symbol_names import dynamic_tags, file_offset | ||
| from test_windows_import_modules import dynamic, executable, provider | ||
|
|
||
|
|
||
| HASH = 0xA00 | ||
| SYMBOLS = 0xC00 | ||
| STRINGS = 0xD00 | ||
|
|
||
|
|
||
| def module(names=("alpha", "bravo", "charlie"), bucket_count=3): | ||
| image = provider(42) | ||
| image.extend(bytes(0x1000 - len(image))) | ||
| struct.pack_into("<QQ", image, 120 + 32, 0xA00, 0xA00) | ||
| names = sorted(names, key=lambda name: name_hash(name + "#A#B") % bucket_count) | ||
| strings = bytearray(b"\0") | ||
| buckets = [0] * bucket_count | ||
| chains = [] | ||
| bloom = 0 | ||
| for index, name in enumerate(names, 1): | ||
| code = 0x400 + index * 16 | ||
| image[code:code + 6] = b"\xb8" + struct.pack("<I", 40 + index) + b"\xc3" | ||
| struct.pack_into("<IBBHQQ", image, SYMBOLS + index * 24, | ||
| len(strings), 0x12, 0, 1, 0x1000 + index * 16, 6) | ||
| qualified = name + "#A#B" | ||
| strings.extend(qualified.encode() + b"\0") | ||
| hashed = name_hash(qualified) | ||
| bucket = hashed % bucket_count | ||
| if buckets[bucket] == 0: | ||
| buckets[bucket] = index | ||
| if chains: | ||
| chains[-1] |= 1 | ||
| chains.append(hashed & ~1) | ||
| bloom |= (1 << (hashed % 64)) | (1 << ((hashed >> 5) % 64)) | ||
| if chains: | ||
| chains[-1] |= 1 | ||
| struct.pack_into("<IIIIQ", image, HASH, bucket_count, 1, 1, 5, bloom) | ||
| struct.pack_into("<" + "I" * (len(buckets) + len(chains)), image, HASH + 24, | ||
| *buckets, *chains) | ||
| image[STRINGS:STRINGS + len(strings)] = strings | ||
| tags = [(5, STRINGS + 0x1A00), (10, len(strings)), (6, SYMBOLS + 0x1A00), (11, 24), | ||
| (0x6FFFFEF5, HASH + 0x1A00), (7, 0x2300), (8, 0), (9, 24)] | ||
| dynamic(image, 0x600, 176, tags) | ||
| return image, names | ||
|
|
||
|
|
||
| def main(): | ||
| relinker = Path(sys.argv[1]).resolve() | ||
| with tempfile.TemporaryDirectory(prefix="anyps5-guest-gnu-hash-") as directory: | ||
| work = Path(directory) | ||
|
|
||
| def convert(label, windows, image, names): | ||
| case = work / label / str(windows) | ||
| modules = case / "sce_module" | ||
| modules.mkdir(parents=True) | ||
| guest = modules / "provider.prx" | ||
| guest.write_bytes(image) | ||
| source = case / "input.elf" | ||
| source.write_bytes(executable("provider.prx", names[-1] + "#A#B") if names else main_fixture()) | ||
| output = case / ("output.exe" if windows else "output.elf") | ||
| result = subprocess.run([str(relinker), *(["--windows"] if windows else []), str(source), str(output)], | ||
| capture_output=True, text=True, timeout=10) | ||
| assert guest.read_bytes() == image | ||
| return result, output, case / "app0/sce_module/provider.prx.guest.prx" | ||
|
|
||
| for label, requested, bucket_count in (("multiple-buckets", ("alpha", "bravo", "charlie"), 3), | ||
| ("one-chain", ("alpha", "bravo", "charlie"), 1), | ||
| ("one-symbol", ("alpha",), 3), | ||
| ("no-hashed-symbols", (), 3)): | ||
| image, names = module(requested, bucket_count) | ||
| for windows in (False, True): | ||
| result, output, artifact = convert(label, windows, image, names) | ||
| assert result.returncode == 0, (label, windows, result.stdout, result.stderr) | ||
| if windows and os.name == "nt": | ||
| run = subprocess.run([str(output)], capture_output=True, text=True, timeout=10) | ||
| assert run.returncode == (40 + len(names) if names else 42), (label, run.returncode, run.stdout, run.stderr) | ||
| if not windows: | ||
| data = artifact.read_bytes() | ||
| tags = dynamic_tags(data) | ||
| symbol_count, = struct.unpack_from("<I", data, file_offset(data, tags[4]) + 4) | ||
| assert symbol_count == len(names) + 1, (label, symbol_count) | ||
|
|
||
| for label, offset, value, error in (("zero-buckets", 0, 0, "Invalid GNU hash header"), | ||
| ("zero-bloom", 8, 0, "Invalid GNU hash header"), | ||
| ("zero-first-symbol", 4, 0, "Invalid GNU hash header"), | ||
| ("large-bloom", 8, 0xFFFFFFFF, "Unmapped ELF address")): | ||
| image, names = module() | ||
| struct.pack_into("<I", image, HASH + offset, value) | ||
| result, output, artifact = convert(label, False, image, names) | ||
| assert result.returncode == 2 and error in result.stderr, (label, result.stderr) | ||
| assert not output.exists() and not artifact.exists() | ||
|
|
||
| image, names = module() | ||
| struct.pack_into("<Q", image, 0x600 + 4 * 16 + 8, 0x29E0) | ||
| struct.pack_into("<IIIIQII", image, 0xFE0, 1, 1, 1, 5, 0, 1, 2) | ||
| result, output, artifact = convert("unterminated-chain", False, image, names) | ||
| assert result.returncode == 2 and "Unmapped ELF address" in result.stderr, result.stderr | ||
| assert not output.exists() and not artifact.exists() | ||
| print("GNU hash guest module tests passed") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When one planned output is a dangling symlink to another planned output that does not exist yet,
weakly_canonicalleaves the symlink unresolved andexistsis false for both paths, so this check reports no collision. For example, withoutput.exesymlinked to the absentwindows-diagnostics-imports.txt, diagnostics are written first and the later executable write follows the symlink and silently overwrites them. Account for dangling symlink targets before deciding the destinations are distinct.Useful? React with 👍 / 👎.