From 35bf15c510cb50ee8152d911c686e5eccdb95c15 Mon Sep 17 00:00:00 2001 From: johnohhh1 <147962948+johnohhh1@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:11:11 -0400 Subject: [PATCH] fix(tools): recognize guest module identities in import audit Resolve module dependencies through ELF SONAMEs and declared SCE module identities, preserving exact-name precedence and rejecting ambiguity. Keep each module import counted once across aliases and repeated inputs. Middle Ages Ping Pong (PPSA28319) reaches a rendered match on current main, but its Il2CppUserAssemblies.prx declares Il2cppUserAssemblies. The audit falsely reported four missing imports and a missing module. Add section-based and sectionless regression coverage and document the matching rules. --- docs/user/USAGE.md | 2 + tools/import_audit.py | 65 +++++++++--- tools/tests/test_import_audit.py | 163 ++++++++++++++++++++++++++++++- 3 files changed, 213 insertions(+), 17 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 8953f29bb746..8dfbd41f5bab 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -382,6 +382,8 @@ Module registries mark ELF weak imports with `"weak": true`, allowing the audit' ELF modules in `--modules` are audited regardless of filename: their undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries they need must have a file in `--libs` or `--modules`. Ordinary non-module sidecars are ignored; malformed ELF, PE/SELF containers and non-ELF files with module extensions remain errors. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. +Module dependencies also resolve through their declared ELF SONAME or SCE module identity. Exact filenames and SONAMEs take precedence over identities; ambiguous names fail the audit. Each module's imports are counted once even when several names refer to it. + Each registry has a companion with the same prefix and the suffix `.needed.json`, listing its host library dependencies, including declared `DT_NEEDED` entries with no imports. Dependencies supplied by converted bundled modules, including their aliases, are omitted. The executable's companion includes host dependencies of its bundled modules. The audit reads these companions automatically; keep them beside the registries. Older registries without companions still work, but cannot report dependencies that have no imports; regenerate them with `--registry` to include those dependencies. The report also lists needed libraries that have no file in `--libs` or `--modules`, including dependencies with zero imports. These do not increase the import or reference counts. Windows can skip a missing dependency when no import needs it; Linux still needs the file. The audit also lists imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows. diff --git a/tools/import_audit.py b/tools/import_audit.py index be6cdf0efcdf..d45531861427 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -14,6 +14,7 @@ ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" MODULE_SUFFIX = "#" +MODULE_EXTENSIONS = (".prx", ".sprx", ".suprx", ".debug_prx") CLASSES = ("implemented", "stub", "absent", "module") PE_DIRECTORY_OFFSET = {0x20b: 112, 0x10b: 96} @@ -57,7 +58,18 @@ def elf_sections(path, data): return [unpack("= len(data): - imports, _, needed = elf_program_dynamic_symbols(path, data) + imports, _, needed = elf_program_dynamic_symbols(path, data, module_names) return imports, needed sections = elf_sections(path, data) symbols = set() @@ -214,6 +231,7 @@ def elf_imports(path, data): break if tag == 1: needed.add(cstring(data, strings_offset + value, strings_offset + strings_size, path, "needed library name")) + add_module_name(path, data, strings_offset, strings_size, tag, value, module_names) return symbols, needed @@ -429,10 +447,12 @@ def module_files(directory): raise AuditError(f"{file}: expected ELF module (first bytes {magic!r})") -def read_modules(directories): +def read_modules(directories, needed_names=()): imports = {} exports = defaultdict(set) images = {} + aliases = {} + identities = defaultdict(set) for directory in directories: for file in module_files(directory): data = file.read_bytes() @@ -443,16 +463,34 @@ def read_modules(directories): raise AuditError(f"ambiguous guest module {file.name}: {previous} and {file} have different contents") continue images[file.name] = (file, digest) + names = {"sonames": set(), "identities": set()} if data.startswith(b"MZ"): imports[file.name] = ([], []) - names = pe_exports(file, data) + exported = pe_exports(file, data) else: - symbols, needed = elf_imports(file, data) + symbols, needed = elf_imports(file, data, names) imports[file.name] = (sorted(symbols), sorted(needed)) - names = elf_exports(file, data) - for name in names: + exported = elf_exports(file, data) + for name in {file.name} | names["sonames"]: + if name in aliases and aliases[name] != file: + raise AuditError(f"ambiguous guest module name {name}: {aliases[name]} and {file}") + aliases[name] = file + for name in names["identities"]: + identities[name].add(file) + for name in exported: exports[name.split(MODULE_SUFFIX, 1)[0]].add(file.name) - return imports, {name: sorted(files) for name, files in exports.items()} + requested = set(needed_names) | {name for _, needed in imports.values() for name in needed} + for name in sorted(requested): + if name in aliases: + continue + suffix = next((extension for extension in MODULE_EXTENSIONS if name.endswith(extension)), "") + stem = name[:-len(suffix)] if suffix else name + providers = identities.get(name, set()) | identities.get(stem, set()) + if len(providers) > 1: + raise AuditError(f"ambiguous guest module identity {name}: " + ", ".join(str(file) for file in sorted(providers))) + if providers: + aliases[name] = next(iter(providers)) + return imports, {name: sorted(files) for name, files in exports.items()}, set(aliases) def nids_of(name, aliases): @@ -751,12 +789,13 @@ def main(argv=None): needed = sorted({library for path in dependency_registries for library in read_needed_registry(path)}) exports, libraries = built_libraries(args.libs) host_dependencies, windows = built_host_dependencies(args.libs) - module_imports, module_exports = read_modules(args.modules) - modules = set(module_imports) - modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} + module_imports, module_exports, modules = read_modules(args.modules, set(needed) | {library for _, library, _ in imports if library}) + host_modules = set(module_imports) + host_modules |= {name.removesuffix(".guest.prx") for name in host_modules if name.endswith(".guest.prx")} + modules |= host_modules modules = ModuleNames(modules, fold=bool(windows)) records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed) - missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules, windows) + missing_hosts = audit_host_dependencies(host_dependencies, libraries, host_modules, windows) if args.names: attach_names(records, load_names(args.names)) summary = summarize(records, sum(record["references"] for record in records), missing, missing_hosts) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index adf98d674225..c64c6e761626 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -23,7 +23,7 @@ MESSAGE_NAME = "sceMsgDialogProgressBarSetValue" -def build_elf(symbols, needed=()): +def build_elf(symbols, needed=(), module_names=()): strings = b"\0" entries = [struct.pack("