Skip to content

feat(nid): patch NIDs in Mach-O libraries - #2247

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
mugurc:macos-nid
Oct 10, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
mugurc:macos-nid

Conversation

@mugurc

@mugurc mugurc commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

What

nid_patcher turns the _nid_postfix names of a library into NIDs for ELF and PE. This adds Mach-O, so that a library built on macOS gets its NIDs too. It is the piece of the macOS port that ninja libs needs there; the build changes that use it come in separate PRs. Nothing changes for ELF and PE.

  • A Mach-O dylib keeps its exports in a dyld export trie and its imports in the chained fixups, so the patcher works on those. The trie is read, every exported C name (without Mach-O's leading underscore) is resolved to its NID, and the trie is rebuilt the way ld64 lays it out: node offsets are uleb128, so the layout is repeated until it is stable. A trie that no longer fits is appended to __LINKEDIT, which has to end the file, and a signed image is refused.
  • Weak definitions that are not marked _nid_postfix or _nid_no_patch keep their names, because dyld coalesces those across images by name.
  • In the chained imports, names bound to a .prx dylib that are guest facing (a _nid_postfix name or an sce prefix) are patched in place. A name that would grow, or that shares storage with another import, is an error. An image that uses bind opcodes instead of chained fixups is refused.
  • --preserve-exports reads a reference Mach-O the way it reads ELF and PE references.

Tested

  • New test nid_patcher_macho: builds small Mach-O dylibs byte by byte and runs nid_patcher on them. It checks the rebuilt export trie (including a weak export that must stay), the patched and untouched chained imports, a trie that grows and is appended to __LINKEDIT, --preserve-exports, and the three refusals (signed image, bind opcodes, an import NID longer than its name). It needs no macOS. I also ran it against two copies of the patcher with a deliberate break each (no import patching, weak exports patched), and both fail it.
  • macOS 26.5 (M5 Max): the patcher turns all 121 libraries of the macOS port into the same bytes the port builds, checked with cmp, and the port's patched_libraries test loads them all.
  • Linux and Windows: the full build and ctest, run by manual dispatch on my fork: https://github.com/mugurc/AnyPS5/actions/runs/37931715161 (Linux 508 of 508, Windows 517 of 517, nid_patcher_macho included). The relinker jobs on a pull request do not build nid_patcher, so they do not cover this change.
  • No commercial title was tested; I have no PS5.

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 (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

nid_patcher turns the _nid_postfix names of a library into NIDs for ELF
and PE. A library built on macOS is a Mach-O dylib, whose exports are in
a dyld export trie and whose imports are in the chained fixups, so it
gets its own patcher.

- The export trie is read, every exported C name (without Mach-O's
  leading underscore) is resolved to its NID, and the trie is rebuilt the
  way ld64 lays it out: the offsets are uleb128, so the layout is repeated
  until it is stable. A trie that no longer fits is appended to
  __LINKEDIT, which has to end the file, and a signed image is refused.
- Weak definitions that are not marked _nid_postfix or _nid_no_patch keep
  their names: dyld coalesces those across images by name.
- In the chained imports, names bound to a .prx dylib that are guest
  facing (a _nid_postfix name or an sce prefix) are patched in place; a
  name that would grow, or that shares storage with another import, is an
  error. Images that use bind opcodes instead of chained fixups are
  refused.
- ExportExclusions reads the exports of a reference Mach-O, so the
  exclusion list works for libraries built on macOS as well.
@popcoder565

Copy link
Copy Markdown
Contributor

Native Windows run, since no Windows results were posted. Setup: Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release. Head f3c51474, merged onto main d70b899 together with #1324, #2039 and #2193.

🤖 Addressed by Claude Code

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the build and ctest scoped to core/libs (11/11, nid_patcher_macho included), a clean merge onto current main, and popcoder565's native Windows run of the new test. ELF/PE paths are unchanged and the Mach-O test pins the rebuilt trie, the patched and untouched chained imports, --preserve-exports and the three refusals. Merging.

@adriadam10
adriadam10 merged commit dac3aa5 into boykopovar:main Oct 10, 2026
7 checks passed
SleglJan pushed a commit to SleglJan/AnyPS5 that referenced this pull request Oct 10, 2026
feat(nid): patch NIDs in Mach-O libraries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants