From b9acf348014cfb9e55d62abffc8739c004cc8600 Mon Sep 17 00:00:00 2001 From: Nixitov Date: Tue, 13 Oct 2026 18:06:27 +0200 Subject: [PATCH] fix(tools): do not report weak imports as a title's current stop A weak import is optional: the entry stub leaves it unresolved and the loader sees null, so the title runs. The relinker now records the symbol binding in the call registry and import_audit skips weak imports when computing the current stop. regie5 (PPSA99520) weak-links sceLncUtilLaunchApp and was reported as stopping there while it runs past it. --- core/relinker/domain/include/domain/Types.hpp | 1 + .../src/output/CallRegistryWriter.cpp | 3 ++- .../src/pipeline/RelinkerPipeline.cpp | 1 + .../relinker/tests/test_call_registry.py | 21 ++++++++++++++- tools/import_audit.py | 15 ++++++----- tools/tests/test_import_audit.py | 27 +++++++++++++++---- 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 31405091c5..e488940a5f 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -112,6 +112,7 @@ struct CallRegistryEntry { FileByteOffset TargetOffset; std::vector CallSites; bool CallSitesResolved; + bool Weak = false; }; } diff --git a/core/relinker/relinker/src/output/CallRegistryWriter.cpp b/core/relinker/relinker/src/output/CallRegistryWriter.cpp index e598056d6e..7c8c909e63 100644 --- a/core/relinker/relinker/src/output/CallRegistryWriter.cpp +++ b/core/relinker/relinker/src/output/CallRegistryWriter.cpp @@ -52,7 +52,8 @@ std::string CallRegistryWriter::WriteCallRegistry(const std::vector& sourceElf entry.TargetSection = ".got"; entry.TargetOffset = ref.RelocationAddress; entry.CallSitesResolved = false; + entry.Weak = ref.Weak; entries.push_back(std::move(entry)); } diff --git a/core/relinker/relinker/tests/test_call_registry.py b/core/relinker/relinker/tests/test_call_registry.py index c12ad56ff0..a3d02c7d28 100644 --- a/core/relinker/relinker/tests/test_call_registry.py +++ b/core/relinker/relinker/tests/test_call_registry.py @@ -35,9 +35,28 @@ def main(): entries = json.loads(registry.read_text(encoding="utf-8")) expected = [{"nid": name, "library": "", "relocationType": "R_X86_64_GLOB_DAT", "relocationOffset": "0x700", "targetSection": ".got", "targetOffset": "0x300", - "callSites": ["0x200"], "callSitesResolved": True}] + "callSites": ["0x200"], "callSitesResolved": True, "weak": False}] if entries != expected: raise AssertionError((name, options, entries)) + strings = b"\0lib.so\0" + names[0].encode("utf-8") + b"\0" + image = fixture(str_size=len(strings), table_bytes=strings) + struct.pack_into(" 2: + record["weak"] = entry[2] + records.append(record) + path.write_text(json.dumps(records), encoding=encoding) class Workspace: @@ -478,6 +484,17 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["stops"], [{"registry": str(registry), "nid": None, "library": None, "name": None}]) self.assertIn(f" {registry}: none", out) + def test_a_weak_unresolved_import_is_not_a_stop(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry = work.root / "game.json" + write_registry(registry, [("BBBBBBBBBBB", "libSceA.prx", True), ("CCCCCCCCCCC", "libSceA.prx")]) + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["stops"], [{"registry": str(registry), "nid": "CCCCCCCCCCC", "library": "libSceA.prx", "name": None}]) + def test_removing_an_export_flips_the_class(self): work = self.work registry = work.root / "game.json" @@ -510,11 +527,11 @@ class ImportAuditTests(unittest.TestCase): nid = nid_names.compute_nid("sceTwinCall") stubs = import_audit.source_stubs(source) exports = {nid: ["libA.prx", "libB.prx"]} - records, _ = import_audit.audit([(nid, "libB.prx")], {nid: ["libB.prx"]}, stubs, {"libB.prx"}, set()) + records, _ = import_audit.audit([(nid, "libB.prx", False)], {nid: ["libB.prx"]}, stubs, {"libB.prx"}, set()) self.assertEqual((records[0]["class"], records[0]["name"]), ("stub", "sceTwinCall")) - records, _ = import_audit.audit([(nid, "libA.prx")], {nid: ["libA.prx"]}, stubs, {"libA.prx"}, set()) + records, _ = import_audit.audit([(nid, "libA.prx", False)], {nid: ["libA.prx"]}, stubs, {"libA.prx"}, set()) self.assertEqual((records[0]["class"], records[0]["name"]), ("implemented", None)) - records, _ = import_audit.audit([(nid, "libB.prx")], exports, stubs, {"libA.prx", "libB.prx"}, set()) + records, _ = import_audit.audit([(nid, "libB.prx", False)], exports, stubs, {"libA.prx", "libB.prx"}, set()) self.assertEqual(records[0]["class"], "implemented") def test_formatted_export_aliases_remain_stubs(self): @@ -802,7 +819,7 @@ class ImportAuditTests(unittest.TestCase): with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid NID 'bad\\x01nid'"): import_audit.read_registry(path) write_registry(path, [("AAAAAAAAAAA#A#B", "libSceA.prx")], encoding="utf-8-sig") - self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx")]) + self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx", False)]) def test_names_never_download(self): work = self.work