Skip to content

fix(libc): find a relinked module by stem when the exact name misses - #6226

Closed
lendinglibrary wants to merge 1 commit into
boykopovar:mainfrom
lendinglibrary:pr/unity-module-names
Closed

lendinglibrary wants to merge 1 commit into
boykopovar:mainfrom
lendinglibrary:pr/unity-module-names

Conversation

@lendinglibrary

Copy link
Copy Markdown
Contributor

What

A title asks for a module by its console name, and the relinker names the converted module after the ELF it was built from. Unity titles load Media/Modules/<name>.prx and Media/Plugins/<name>.prx while the dump holds <name>.elf.guest.prx, so RelinkedModulePath misses and sceKernelLoadStartModule fails with 0x80020002. A Unity IL2CPP title stopped 3 s after start with unable to load /app0/Media/Modules/Il2CppUserAssemblies.prx, error:0x80020002.

After the exact <name>.guest.prx and the case-insensitive lookup (#5892's predecessor f0c3a5c) both miss, the lookup now takes the sibling whose stem (name up to the first dot, ASCII case ignored) equals the requested stem and that ends with .guest.prx, shortest name first. The test loads guest_module_fixture.prx from a directory holding only guest_module_fixture.elf.GUEST.PRX. The assumption about the console's module namespace is recorded in docs/dev/debt/libc.md.

Tested

Linux (Ubuntu, i5-12400, RTX 5070 Ti). guest_dynamic_loader_variant_names and the loader tests pass. A Unity IL2CPP title whose dump holds only <name>.elf.guest.prx: loads Il2CppUserAssemblies.prx and PS5Util.prx, plays its intro videos, reaches the main menu and starts a game. A title whose dump already has <name>.prx.guest.prx copies: unchanged. Windows: not tested.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw; silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@jotitadev jotitadev left a comment

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.

Static check on macOS (Apple M2 Max, macOS 27.2, APFS, Apple clang 21 / libc++), head 7b2385aa0. Not built: main does not link on macOS yet. I compiled RelinkedSiblingByStem verbatim from the PR in a small driver.

With libc++, the shortest-name-first rule does not hold when two or more siblings match the stem. The loop shares one std::error_code between directory_iterator, is_regular_file and equivalent. libc++ implements LWG 2937, so equivalent(existing, missing, ec) sets ec to ENOTSUP when the requested path does not exist, which is always the case on this fallback path. On the next iteration, if (error || ...) continue; sees that stale value and skips every remaining entry, so the first stem match in directory order wins.

Directory with Il2CppUserAssemblies.{elf,prx.elf,sprx.elf}.guest.prx, request Il2CppUserAssemblies.prx. arm64 and x86_64 under Rosetta give the same result:

visit Il2CppUserAssemblies.sprx.elf.guest.prx (error=0)
visit Il2CppUserAssemblies.prx.elf.guest.prx (error=45)
visit Il2CppUserAssemblies.elf.guest.prx (error=45)
picked: Il2CppUserAssemblies.sprx.elf.guest.prx

equivalent(<existing file>, <missing file>, ec) alone gives eq=0 err=45 Operation not supported.

I did not check libstdc++. With a single candidate the result is right, so the new test would not catch this. A local std::error_code for the equivalent call, or clearing error at the top of each iteration, keeps the shortest-name rule that docs/dev/debt/libc.md describes.

Written with AI assistance (Claude Code). I ran the snippets above on my machine.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@simonbbby

Copy link
Copy Markdown
Contributor

The same shared-error issue is present on Linux/libstdc++, not only libc++. Checked exact head 7b2385aa00814baeb5d63344f61f3e2710293357, Ubuntu 24.04 / GCC 13.3, by compiling the unchanged FoldCase, NameStem and RelinkedSiblingByStem bodies against the real filesystem.

For Il2CppUserAssemblies.prx (absent), with the three regular sibling files:

VISIT Il2CppUserAssemblies.sprx.elf.guest.prx
VISIT Il2CppUserAssemblies.prx.elf.guest.prx
VISIT Il2CppUserAssemblies.elf.guest.prx
PICK Il2CppUserAssemblies.sprx.elf.guest.prx
RAW_EQ 0 ERROR 2 No such file or directory

Here equivalent(existing, missing, ec) leaves ENOENT (2). The next iteration sees that stale error before it can examine the shorter sibling. Each of the three candidates is selected correctly when it is the only sibling, ruling out the stem/suffix filter. The filename-length comparison does not need changing.

The review's clear-at-the-top option also covers another per-entry error:

Helper change Three valid candidates, request absent Dangling symlink visited before one valid candidate
None wrong longest candidate, 30/30 runs —
Separate error code only for equivalent correct in all six fixture directories no candidate found
error.clear() at the top of each iteration shortest candidate, 30/30 valid candidate, 5/5

The remaining failure in the middle row comes from is_regular_file(error) on the dangling symlink: that error also leaks into the next entry. So either reset per iteration as suggested, or keep every per-entry status/identity error separate from directory enumeration.

Tested minimal change:

     for (const auto& sibling : std::filesystem::directory_iterator(parent, error)) {
+        error.clear();
         if (error || !sibling.is_regular_file(error)) continue;

The 30-run cases are five processes for each of six directories populated in different creation orders. This filesystem enumerated the same longest-first order in all six; these are not six different observed enumeration orders. An empty-directory control returns no candidate with the patch.

Scope: standalone execution of the actual helper bodies with ordinary test files and a real dangling symlink. No full runtime/library build, module loading or title result is claimed. No repository source was changed.

This was referenced Oct 15, 2026
@lendinglibrary

Copy link
Copy Markdown
Contributor Author

Fixed in the amended commit: the shared error code is cleared at the top of each iteration, so a stale result from equivalent or from is_regular_file on a dangling symlink no longer skips the remaining siblings. The test directory now holds three stem matches: the real module (shortest name), a longer non-module file and, on non-Windows, a longer dangling symlink; the load succeeds only when the shortest valid one is picked. Thanks both for the reproductions.

A title asks for a module by its console name and the relinker names the
converted module after the ELF it was built from: Unity IL2CPP titles load
Media/Modules/<name>.prx and Media/Plugins/<name>.prx while the dump holds
<name>.elf.guest.prx, so sceKernelLoadStartModule failed with 0x80020002
and the title stopped 3 s after start. After the exact and the
case-insensitive <name>.guest.prx lookups miss, take the sibling whose stem
matches the request and ends with .guest.prx, shortest name first.
@lendinglibrary

Copy link
Copy Markdown
Contributor Author

Rebased on current main after eac8bfc. The stem rule is now a second NameMatch pattern (stem = true) in the same single directory listing: a sibling ending in .guest.prx whose stem matches the request, regular files only, shortest name wins; the exact case-insensitive match keeps precedence. No extra listing and no separate loop any more. Test and conventions pass on Linux.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@simonbbby

Copy link
Copy Markdown
Contributor

Retested the rewritten NameMatch path at exact head 138a74ca5a701d1a52063702e434ec6e99a5b13d. The two filesystem failures from my earlier report are resolved on Linux, and the unchanged selector bodies also pass native macOS/libc++ checks.

Linux — Ubuntu 24.04, GCC 13.3. Compiled the complete unchanged production General.cpp with function/data sections, linked with garbage collection, and called the actual exported RelinkedModulePath_nid_no_patch:

  • Three sibling variants, longest encountered before shortest: shortest selected in 30/30 calls across six directories populated in different creation orders. These are not six distinct observed enumeration orders.
  • A dangling same-stem symlink visited before the valid sibling: 5/5 pass.
  • Exact-name and case-insensitive exact-name precedence over the stem fallback; ambiguous case-insensitive exact names throw; matching directories are excluded; empty and unrelated-only directories retain the requested path; a symlink to a regular module works. Each candidate alone also works. 45 cases total, all pass.
  • The selected valid file is a locally compiled synthetic C shared library. The loading cases use native dlopen, resolve GuestModuleAdd, require add(19,23) == 42, and close it. The longer decoy files contain invalid-module text.

Matched original-head control: rebuilding only General.cpp from 7b2385aa0 against the same probe still selects the longest sibling in the first case and returns the unresolved request after the broken symlink in the second. Both fail; the rewritten object passes.

macOS 27.2 arm64 / Apple clang 21 / libc++ — the complete translation unit still fails on unrelated CLOCK_REALTIME_COARSE and stat::st_mtim uses. I instead compiled the unchanged selector/helper bodies from this head. 43 cases pass, including the 30 shortest-name calls, five broken-symlink-first calls and native synthetic-library loading. The two Linux case-collision/uppercase-only cases were excluded from this macOS run.

No repository source changed. This verifies filesystem selection and native loading of a synthetic library, not guest dlopen_nid_postfix, the full runtime, Windows, or a title replay. The previously reported stale-error blocker is no longer reproduced at this head.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks. One fix is needed before merge, and the branch needs a rebase:

The stem is cut at the first dot, not the last. For a request like Engine.Render.prx that misses, the stem becomes Engine, so the lookup can quietly load Engine.Core.prx.guest.prx, a different module. Please strip only the last extension (std::filesystem::path::stem() does this), and add a test with a dotted name that would load the wrong module with the current code.

Also, #6544 changes the same function (RelinkedModulePath) for a different layout (a converted copy in a prx/ or sce_module/ subdirectory). When both land, the lookup order should be: exact match, then same directory ignoring case, then nested, then stem. Please keep the stem match last.

The branch is about 600 commits behind main, so please rebase onto current main before we run CI.

@TheTerr4

Copy link
Copy Markdown

Windows runtime check. Windows 11 Pro 10.0.26200, Ryzen 7 7800X3D, WinLibs GCC 15.2.0 r7 (UCRT), Release.

This PR's head 138a74ca5 merged cleanly into main 385ed71f2; the full build and libs both exit 0.

  • guest_dynamic_loader and guest_dynamic_loader_variant_names pass, three repeated -j1 runs.
  • Negative control: with libc/src/General.cpp restored to main's version, guest_dynamic_loader_variant_names aborts (exit 0xc0000409); guest_dynamic_loader still passes.
  • Not run: the full suite, Linux, macOS.

AI-assisted: yes (Claude Code).

@xXPBSXx

xXPBSXx commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

I carried this onto current main in 8626: it keeps your original commit and author and adds one commit for the review points (the stem now drops only the last extension, plus a test with a dotted name). I did not close this PR; if you prefer to push those changes here, 8626 can be closed. Branch: https://github.com/xXPBSXx/AnyPS5/tree/carry/6226-relinker-stem.

AI-assisted (Claude Code).

oneandonlydean added a commit that referenced this pull request Oct 21, 2026
fix(libc): find a relinked module by stem when the exact name misses (carries #6226)
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@lendinglibrary thanks for this. Your stem fallback has landed on main through #8626, which carries your commit with your authorship, together with a guard that only uses the stem when the exact name neither matched nor was ambiguous. Closing this one as superseded by #8626.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build docs libc libkernel needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants