Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/user/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
65 changes: 52 additions & 13 deletions tools/import_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -57,7 +58,18 @@ def elf_sections(path, data):
return [unpack("<IIQQQQIIQQ", data, section_offset + index * 64, path, f"section header {index}") for index in range(section_count)]


def elf_program_dynamic_symbols(path, data):
def add_module_name(path, data, offset, size, tag, value, names):
if names is None or tag not in (14, 0x6100000d, 0x61000043):
return
position = value if tag == 14 else value & 0xffffffff
name = cstring(data, offset + position, offset + size, path, "module name")
forbidden = "\\:$\r\n" if tag == 14 else "/\\:$\r\n"
if not name or any(character in name for character in forbidden):
raise AuditError(f"{path}: invalid module name {name!r}")
names["sonames" if tag == 14 else "identities"].add(name)


def elf_program_dynamic_symbols(path, data, module_names=None):
header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header")
ident, program_offset, program_size, program_count = header[0], header[5], header[9], header[10]
if ident[6] != 1 or header[3] != 1:
Expand Down Expand Up @@ -85,6 +97,7 @@ def file_offset(address, size, what):
raise AuditError(f"{path}: dynamic segment size {dynamic_size} is not a multiple of 16")
dynamic = {}
needed_offsets = []
named_entries = []
for position in range(0, dynamic_size, 16):
tag, value = unpack("<qQ", data, dynamic_offset + position, path, "dynamic entry")
if tag == 0:
Expand All @@ -93,13 +106,17 @@ def file_offset(address, size, what):
needed_offsets.append(value)
else:
dynamic[tag] = value
if tag in (14, 0x6100000d, 0x61000043):
named_entries.append((tag, value))
string_address = dynamic.get(5)
string_size = dynamic.get(10)
symbol_address = dynamic.get(6)
symbol_size = dynamic.get(11)
if None in (string_address, string_size, symbol_address, symbol_size) or symbol_size != 24 or string_size == 0:
raise AuditError(f"{path}: incomplete dynamic string or symbol table")
string_offset = file_offset(string_address, string_size, "dynamic string table")
for tag, value in named_entries:
add_module_name(path, data, string_offset, string_size, tag, value, module_names)

symbol_count = None
if 4 in dynamic:
Expand Down Expand Up @@ -183,10 +200,10 @@ def elf_exports(path, data):
return names


def elf_imports(path, data):
def elf_imports(path, data, module_names=None):
header = unpack("<16sHHIQQQIHHHHHH", data, 0, path, "ELF header")
if not header[6] or header[6] >= 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()
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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()
Expand All @@ -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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Resolve existing filename aliases before rejecting identity ambiguity

This exact-name guard bypasses the existing suffix matcher. With a physical Declared.prx, two other modules declaring identity Declared, and a registry plus .needed.json requesting Declared.sprx, current main exits 0 but this head exits 2 with ambiguous guest module identity Declared.sprx. .suprx and .debug_prx reproduce it too; the exact .prx control passes. The relinker installs the filename suffix alias in neededAliases before identity resolution (GuestModuleBuilder.cpp:85,146 at this head). Please preserve that filename precedence before checking identity collisions and add a cross-suffix regression; unrelated identities should not invalidate an already-resolved physical module.

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):
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep declared aliases out of Windows filename case folding

The returned alias set is later wrapped in ModuleNames(..., fold=bool(windows)), so declared names inherit filename case folding. With a PE built library and Provider.prx declaring SCE identity Declared, a request for declared.prx correctly exits 1 by itself, but adding a valid Declared.prx reference makes both imports classify as module and the audit exit 0. A SONAME Declared.prx similarly accepts declared.prx. In the relinker, findGuest folds physical filenames only and matchesIdentity compares identities exactly (GuestModuleBuilder.cpp:88,140). Please retain exact matching for declared aliases separately from physical filename matching; adding another valid reference must not hide the unresolved import.

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)
Expand Down
Loading
Loading