Skip to content

fix(libs): give each .native wrapper a dependency edge to its plain twin - #1009

Closed
Dextroid17 wants to merge 1 commit into
boykopovar:mainfrom
Dextroid17:fix/native-wrapper-dependency-edge
Closed

Dextroid17 wants to merge 1 commit into
boykopovar:mainfrom
Dextroid17:fix/native-wrapper-dependency-edge

Conversation

@Dextroid17

Copy link
Copy Markdown
Contributor

What

Gives each .native wrapper a DT_NEEDED edge to its plain twin (the first option in #906). Closes #906.

A guest module's DT_NEEDED names the .native wrapper, but symbols that only the plain twin exports were unreachable — the wrapper had no edge to the twin, so the defining object never entered the new module's lookup scope and RTLD_NOW binding failed:

31418  [dl] dlopen(/app0/Media/Plugins/PSN.prx, 0x2) -> (nil) error: undefined symbol: wTpfglkmv34

All nine wrappers lacked the edge (libSceAjm.native, libSceAudiodec.native, libSceAvPlayer.native, libSceFont.native, libSceMsgDialog.native, libSceNgs2.native, libSceSaveDataDialog.native, libSceSaveData.native, libSceShare.native), and all nine plain twins exist.

The change links each .native library against its plain twin on ELF targets. Windows is left alone: PE resolution binds against every module already loaded and the relinker points guest imports at the implementing library, so the edge is not needed there.

This is option 1 of the three listed in the issue; option 2 (the relinker emits the implementing library in the converted guest module's DT_NEEDED) would also work and touches the relinker instead of the library graph. Happy to switch if you prefer that direction.

Tested

  • Linux, CachyOS (Arch), GCC 16.2.1, Release.
  • readelf -d on all nine wrappers: no NEEDED libSceX.prx before the change, present after.
  • dlopen probe with RTLD_LOCAL (the way a guest module binds), checking /proc/self/maps:
    • before: dlopen(libSceMsgDialog.native.prx) leaves libSceMsgDialog.prx unmapped
    • after: it is mapped (same result for libSceAudiodec.native and libSceAjm.native)
  • cmake --build build --target libs → 106 .prx built.
  • Full suite: ctest --test-dir build -j 8 --timeout 120 → 369/369 pass.
  • Not tested on a title.

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 (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source — not applicable
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

A guest module's DT_NEEDED names the .native wrapper, but symbols that only the
plain twin exports were unreachable: the wrapper had no DT_NEEDED edge to the
twin, so the defining object never entered the new module's lookup scope and
RTLD_NOW binding failed with "undefined symbol" (boykopovar#906).

Link each .native library against its plain twin on ELF targets, where lookup is
scope-based. Windows is unaffected: PE resolution binds against every module
already loaded and the relinker points guest imports at the implementing
library, so the edge is not added there.

Verified by dlopen: before the change, loading libSceMsgDialog.native.prx left
libSceMsgDialog.prx unmapped; after, it is mapped. The same holds for all nine
wrappers, and the full suite (369 tests) passes.
@Dextroid17

Copy link
Copy Markdown
Contributor Author

Closing this: @jeffrey12cali measured on PPSA28997 that this exact approach regresses the title (see #906).

Pulling the plain twin in through a DT_NEEDED edge puts it in the executable's global scope on Linux, and the pairs share NIDs (libSceAvPlayer 30/30 overlapping, libSceSaveData 14/41, libSceMsgDialog 6/7), so other modules rebind to the wrong implementation and the title fails earlier than before. The static checks in the description (readelf, dlopen) all pass and are exactly why they are not sufficient here — the edge is added, and that is the problem.

The twin has to stay out of the global scope. I will pick this up again on that basis — most likely the relinker emitting the implementing library in the converted guest module's DT_NEEDED (the issue's option 2, which is also the stopgap described in #906) rather than a library-graph edge, or the wrapper re-exporting what the plain library provides.

Branch fix/native-wrapper-dependency-edge is left in place for reference. Sorry for the noise, and thank you for the measurement.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux: .native wrapper libraries have no dependency edge to their plain twin, so guest modules cannot bind

1 participant