Skip to content

fix(relinker): rebase the relocated program header table at a fixed Linux base - #6224

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
Agnibha007:fix/linux-fixed-base-relocated-phdrs
Oct 17, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
Agnibha007:fix/linux-fixed-base-relocated-phdrs

Conversation

@Agnibha007

@Agnibha007 Agnibha007 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

--linux-fixed-base rebases the output program header table at the input's e_phoff instead of the table the relinker actually wrote. When the input ELF declares fewer program header slots than the output layout needs, the relinker moves the table to the end of the output and updates e_phoff; the fixed-base rebase then ran against the stale location. This passes the relocated table offset (tableOff) to RebaseProgramHeaders, so every declared program header is rebased regardless of where the table lives.

Fixes #6393.

Why

LinuxElfPatcher::Patch (core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) moves the output program header table to tableOff when RequiredProgramHeaderCount (kept headers + 5 synthetic) exceeds the input's e_phnum, writes the new layout there (layoutRequest.PhOff = tableOff, WriteLayout) and updates e_phoff. The fixed-base step then called RebaseProgramHeaders(buf, phOff, ...) with the original table offset, so:

  • The live table's p_vaddr/p_paddr were never rebased, while e_entry, the dynamic segment addresses and the relocations were. The output is an ET_EXEC whose program headers still describe the image at the original addresses (lowest PT_LOAD vaddr 0) while its entry point is at base+..., so it cannot run at the fixed base, against docs/user/USAGE.md ("every virtual address of the image is offset by it").
  • The rebase wrote writtenPh entries at the original table offset, overwriting input bytes that follow the table. When those bytes decode as headers with large addresses, the conversion aborts with an unrelated error (Program header address 0x4141414141414141 lies outside the Linux user address space at fixed base 0x400000) although the same input converts without the option.

Both halves reproduce deterministically with the existing test_linux_fixed_base.py fixture trimmed to 5 program header slots (1 kept PT_LOAD + 5 synthetic = 6 required > 5, so the table is relocated; SegmentFilter skips PT_DYNAMIC and the PT_SCE_* fillers). With the fixture's default 6 slots the table stays in place, which is why the existing tests never distinguish the two paths.

Tested

Relinker-only build on Linux x86-64 (CMake 4.4.4, GCC 16.2.1, Python 3.14.7), head 2cf75dbbf based on d92a8dad3:

  • Regression test against the unmodified implementation: a temporary worktree of current upstream/main 5d9dcc42d (still passing phOff) was configured relinker-only and built; running the updated test against that binary fails with exit 1, and a focused probe of the grown-table assertions shows the failures they catch — the rebased entry 0x401118 is covered by no PT_LOAD (unmapped) and the lowest output PT_LOAD vaddr is 0x0, not 0x400000. The worktree was discarded afterwards; the PR branch was not modified for this check.
  • After the fix: python3 core/relinker/relinker/tests/test_linux_fixed_base.py build-relinker/core/relinker/relinker passes. The relocated-table case asserts: the output table grew past the input's slot count (e_phnum 5 → 6) and moved (e_phoff is no longer 0x40); every output header equals the plain relink's with p_vaddr/p_paddr shifted by the base; the image starts at the base; the entry point equals the plain relink's entry plus the base; the entry is covered by a file-backed PT_LOAD and that file offset lies inside the output; and the covering segment is executable. The x86-64 execution check to the designed SIGTRAP and the filled-input conversion check are unchanged.
  • ctest --test-dir build-relinker --output-on-failure -j4: 100% tests passed, 0 failed out of 172, 26.15 s total.
  • python3 tools/check_conventions.py --base upstream/main (base 5d9dcc42d): all conventions checks passed.
  • python3 tools/check_test_registration.py build-relinker: 29 test executables, none built and not run.
  • git diff --check: clean.
  • python3 tools/pr_overlap.py --branch fix/linux-fixed-base-relocated-phdrs: only fix(relinker): rebase the grown Linux program header table #6403 overlaps (test_linux_fixed_base.py); that PR's author has offered to close it once this coverage is present here (the grown-table assertions proposed there are consolidated in this PR's relocated-table case).

Not run: the full (non-relinker-only) build, Windows or macOS relinker output tests, --to-intel/--to-rosetta conversions, and conversions of real titles. An independent contributor built and ran the focused test on Windows/MinGW-w64 at head 8e5073ebe; that run predates this commit.

Scope and limitations

Linux output only; the fix touches one call in LinuxElfPatcher::Patch. No GPU, Windows, macOS or game behavior was tested, and no real title was converted. The execution check runs only on x86-64 Linux, under the same guard as the existing fixed-base execution check; on other hosts the test verifies output bytes only. The3-slot fixture proposed in #6403 takes the same relocation path (required > e_phnum) as the 5-slot fixture used here, so its coverage was consolidated into the existing relocated-table case rather than duplicated.

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 under Silent stubs in their library's technical debt file (n/a: no library code changed)
  • 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 (n/a)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@fogydes

fogydes commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Independent Windows validation on current PR head 8e5073ebe8de5eb1c90b0165b63da23cbb62774d: configured a fresh relinker-only Release build with MinGW-w64 GCC 15.2.0 and built successfully. The focused linux_fixed_base regression passed (0.52 s). No source changes made.

@Agnibha007

Copy link
Copy Markdown
Contributor Author

Independent Windows validation on current PR head 8e5073ebe8de5eb1c90b0165b63da23cbb62774d: configured a fresh relinker-only Release build with MinGW-w64 GCC 15.2.0 and built successfully. The focused linux_fixed_base regression passed (0.52 s). No source changes made.

Thanks for independently validating the PR on the current head and confirming that both the build and the focused regression test pass. Appreciate the additional validation!

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 6447 core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp

@MarcZX10

Copy link
Copy Markdown
Contributor

I found the same RebaseProgramHeaders(tableOff, ...) correction while investigating the grown-table edge case from #6393, and opened #6403 before noticing the overlap bot report. Your PR is already validated and covers the main relocated-table path; #6403 adds a three-slot fixture where the original table is overwritten plus assertions on the rebased entry and mapped load header. Could you fold those assertions into #6224? I will close #6403 once the coverage is present there, to keep one implementation.

@Agnibha007

Copy link
Copy Markdown
Contributor Author

@MarcZX100 — folded the #6403 coverage into this PR as offered, in 2cf75dbbf (test-only commit; the tableOff correction is unchanged).

Integrated into the existing relocated-table case in core/relinker/relinker/tests/test_linux_fixed_base.py:

  • output table grew past the input's slots — e_phnum of the output (0x38) must exceed the input's (5 → 6), making the "input too few slots" precondition explicit;
  • entry rebased — relocated_entry == plain_entry + BASE (exact form of your grown_entry >= BASE, same wording as the in-place check at line 97);
  • entry mapped by a PT_LOAD — mapped() covers only p_vaddr <= addr < p_vaddr + p_filesz, so it proves a file-backed mapping rather than addr >= base; additionally asserted that the returned file offset lies inside the output and that the covering segment has PF_X.

Your e_phoff != 64 and lowest-PT_LOAD == base assertions were already present (!= 0x40 and the min check in the same case). The 3-slot fixture exercises the identical path (required > e_phnum → relocation) as the 5-slot fixture here, so it was consolidated rather than duplicated — its uniquely failing assertion (0x401118 unmapped) is covered by the mapped() check above.

Verified fail-before/pass-after: a temporary worktree of upstream/main 5d9dcc42d (still phOff) built relinker-only; the updated test fails there with exit 1, and a focused probe shows mapped() rejecting 0x401118 and the lowest PT_LOAD at 0x0. After the fix: focused test passes, ctest 172/172, check_conventions.py --base upstream/main, git diff --check and check_test_registration.py all clean. PR body updated accordingly (including the link to #6393).

Thanks for offering to close #6403 — the coverage is present on this branch now.

Written with AI assistance (OpenCode).

@Dextroid17

Copy link
Copy Markdown
Contributor

Tested 2cf75db, which folds in the #6403 coverage (merge base d92a8da; it merges cleanly into main at 82ddae7), on Linux (CachyOS, kernel 7.2.9), relinker-only Release build with BUILD_TESTING=ON:

  • ctest -R '^linux_fixed_base$': passed, 3 of 3. The full relinker suite passed, 172/172.
  • With LinuxElfPatcher.cpp reset to the merge base (phOff instead of tableOff) and the relinker rebuilt: failed, 3 of 3, on the relocated-table header comparison (the PT_PHDR entry is not shifted). Restored and rebuilt, it passes again.
  • fix(relinker): rebase the grown Linux program header table #6403 at e8c42e3 behaves the same with its own test: passes 3 of 3, fails 3 of 3 on 0x401118 without the change.

I also relinked a real eboot with --linux-fixed-base 0x400000: Ghost of Yōtei (PPSA26344 v01.008). Its input table has 14 slots, and with #6406 (which keeps PT_GNU_RELRO) the output needs 15, so the table moves:

#6406 and this PR merge together without conflicts. #6447 carries the same tableOff line; it merges cleanly with this PR, but conflicts with #6406.

Not run: Windows, macOS, a hardware GPU, a boot past 90 s.

AI-assisted: yes

@oneandonlydean oneandonlydean self-assigned this Oct 17, 2026
@oneandonlydean
oneandonlydean merged commit e1ac330 into boykopovar:main Oct 17, 2026
2 checks passed
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.

relinker: --linux-fixed-base rebases the stale program header table when the table has to grow

5 participants