Skip to content

fix(relinker): reject collisions between generated outputs - #4802

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
ArjenS-LA:fix/relinker-output-collisions
Oct 14, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
ArjenS-LA:fix/relinker-output-collisions

Conversation

@ArjenS-LA

@ArjenS-LA ArjenS-LA commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What

Reject generated outputs that resolve to the same destination before writing executables, guest modules, registries, desktop files, icons, or Windows import diagnostics. Main can report success after one output overwrites another: for example, hard-link the executable and generated module destinations before conversion.

The preflight compares canonical paths and existing file identities. It includes every --executable output and every executable/module registry. A secondary executable named windows-diagnostics-imports with a primary output named output.txt also produces the diagnostics path; this now fails before either output is written.

On supported Windows MinGW, filesystem::equivalent misses hard links with different extension-derived synthetic permissions. The standard-library workaround compares temporary hard links with identical names in reserved sibling directories, then removes them before writing. Read-only identities are rejected before link creation. Existing case variants use actual identity; absent ASCII case variants use a parent-directory case-sensitivity probe on Windows. This does not provide transactional safety against concurrent filesystem mutations.

Branch: fix/relinker-output-collisions; fixed commit: 69375305f500004f5648e683eb012323e6e9ad6b; baseline/current main checked: d8888df9b280a23077ff47d412bd7668c8c8b656.

Tested

Windows x86-64, build 26200; supported WinLibs MinGW-w64 GCC 15.2.0 (x86_64-ucrt-posix-seh, r7), Python 3.13.7, CMake 4.3.1, Ninja 1.13.2. Release relinker-only builds; actual filesystem conversion fixtures, no title or GPU testing.

  • Original collision regression fails against a separate unchanged build of main d8888df9b280a23077ff47d412bd7668c8c8b656: executable/module aliasing incorrectly returns 0. The identical test passes with the fix.
  • New secondary-output regression fails against the offered handoff 1d9394dc3739d6195951cd71e9ed6496446848da normalized onto the same main: primary/secondary executable aliasing incorrectly returns 0. Its binary was preserved before the fix. The identical test passes with all executable outputs registered.
  • The two tests cover 25 original and 32 secondary-output hard-link pairs across ELF and PE output modes, direct diagnostics collisions, preserved inputs and previous outputs, distinct-file success controls, read-only identity rejection, and temporary-directory cleanup. Targeted CTest: 2/2 passed in 4.12 seconds.
  • Complete relinker build succeeded; 155 CTest entries registered, including input_magic, output_collisions, and secondary_output_collisions. Selected suite: 145 passed, 1 skipped (long_paths, Windows long paths disabled), 0 failed, 27.49 seconds. Nine unrelated tooling tests were excluded because they create disposable Git commits under the local daily commit limit; the exclusion expression below lists them.
  • Internal platform skips remain: case-sensitive Windows directory controls, privileged symbolic-link fixtures, Linux-only guest execution, and hardware-oracle platform/compiler cases. Case-only checks for absent paths are Windows-only; case-insensitive macOS/Linux volumes are not covered by that branch. No native Linux/macOS, full-library, title, or hardware validation is claimed for this revision.
  • git diff --check and committed-object tools/check_conventions.py --base origin/main passed. Local results describe the exact fixed commit above; upstream CI is reported separately.

Commands with WinLibs bin on PATH and a working Python interpreter:

cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_CXX_COMPILER=g++ -DPython3_EXECUTABLE=<python.exe>
cmake --build build-relinker --parallel 4
python core/relinker/relinker/tests/test_output_collisions.py <main-relinker.exe>
python core/relinker/relinker/tests/test_secondary_output_collisions.py <preserved-handoff-relinker.exe>
ctest --test-dir build-relinker -N
ctest --test-dir build-relinker --output-on-failure -R "^(output_collisions|secondary_output_collisions)$"
ctest --test-dir build-relinker --output-on-failure -j 4 -E "^(pr_overlap_encoding|progress_history|import_audit|conventions_function_bodies|conventions_wrapped_declarations|conventions_ci_gate|debt_split|conventions_body_bom|pr_overlap_own_branch)$"
python tools/check_conventions.py --base origin/main
git diff --check

Integration

Refreshed all 433 open PRs across five pages with complete changed-file lists, and inspected all 27 shared-file implementations. Tree-only merge previews normalize exact heads onto main d8888df9b280a23077ff47d412bd7668c8c8b656, then compare with that explicit merge base. No temporary commits were created. No duplicate output-collision implementation was found. Seven pairs merge cleanly; twenty are inconclusive because the other PR already conflicts with main. No new pairwise conflict was reported. Exact checked heads and pre-existing main-conflict paths are recorded below.

#4916 at 22b190ae081c5e01119a1f6b3559b64754b18df9 merges cleanly but introduces a semantic integration requirement: append its neededPaths to outputPaths before the collision loop, and extend collision coverage to .needed.json companions when the second PR lands. Its feature is not included in this PR. #5018 at 26c62cc63d404d81392f9f4ab004d3a6cd29fe2e is complementary: Windows, its fallback writes, and desktop/icon writes still need collision protection.

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
  • 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
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes, OpenAI Codex
Exact overlap audit
PR Head Pair result
#3891 67452b252f041456d3b607c3cdce02d98b41bef0 inconclusive: pre-existing main conflicts
#4156 1f466a8f09e73e8dbbc4fd1d89aea152ce10127c inconclusive: pre-existing main conflicts
#4396 1a384f3769510cb8543b6536165748e5d4f5071f inconclusive: pre-existing main conflicts
#4397 dba9140ff98325ff177f67e0d262957a80bdf2fa inconclusive: pre-existing main conflicts
#4422 7b77941beb4d08d589d3543a23cad9107f6f35f0 inconclusive: pre-existing main conflicts
#4423 22705f1f080cffa143b49967f16559d6116692b9 inconclusive: pre-existing main conflicts
#4424 a93a16168870d40080b4924191201702676ccadf inconclusive: pre-existing main conflicts
#4520 8f9544978c3081b76ee96547358d4eeb5e129848 inconclusive: pre-existing main conflicts
#4521 50db14a2e895492f68ec9fd3d97a2bd599439ddf inconclusive: pre-existing main conflicts
#4522 7010bb3101adfdfe021a1181836e63581db7e410 inconclusive: pre-existing main conflicts
#4523 e5ba22ce3338e25b57bd874a670ded9a333324de inconclusive: pre-existing main conflicts
#4566 3625741d1cf6f05897aea07658c9f4f7296493f4 inconclusive: pre-existing main conflicts
#4593 c25a5783e51db770884e90c416c2614f2ef58791 inconclusive: pre-existing main conflicts
#4599 766e0a908207bf6ab66e36dfa14e32524f89d18b inconclusive: pre-existing main conflicts
#4718 007d30827d368e4c0e447d2b004a87f6add78dd9 inconclusive: pre-existing main conflicts
#4740 67bcf02e9e3c9056bc493bf23d2df17b984139b7 inconclusive: pre-existing main conflicts
#4749 b22bd245f1f729afb9ac0fb566c8d4ff75f67e04 inconclusive: pre-existing main conflicts
#4760 0f7e237338d539283973584628167b7a6ac40dba inconclusive: pre-existing main conflicts
#4813 d83b44515937fe992c1dd19d4c440e54950b38b2 inconclusive: pre-existing main conflicts
#4916 22b190ae081c5e01119a1f6b3559b64754b18df9 clean
#5021 4fc8ef602bc15fdf19cc1d97ff76b6b51b8ddd00 inconclusive: pre-existing main conflicts
#5138 5f564173209aec77c82051ada9399066941d1eb8 clean
#5512 9954e58af95a3ae9bc315f56550adbd4ae883e7a clean
#5559 c37a4b8b6b85d94cb9bcc6c75b8b86905ebf3c29 clean
#5614 0093322ba1810dbdf06f5115c0b75ac4fab8d4c0 clean
#5640 a8ed810d0785d9acd6930d09b1fe0448e4876a66 clean
#5683 8b400afc5d5635e5af803c10b5725d7f4405ede7 clean

Pre-existing conflicts with main for #3891, #4156, #4396, #4397, #4422, #4423, #4424, #4520, #4521, #4522, #4523, #4566, #4599, #4718, #4740, #4749, #4760, #4813: CMakeLists.txt, core/libs/prx/libSceAgcDriver/PostCompile.cmake, core/libs/prx/libSceFiber/Export.cpp, core/libs/prx/libSceVideoOut/CMakeLists.txt, core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp, core/libs/prx/libc/src/GuestAllocations.cpp, core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp, core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp, core/libs/prx/libkernel/System/src/SignalSets.cpp, core/libs/tests/GuestMemory.cpp, core/libs/tests/GuestSignalSets.cpp, docs/dev/debt/libkernel.md, docs/dev/debt/relinker.md, docs/dev/debt/shader.md.

Pre-existing conflicts with main for #4593: docs/user/USAGE.md.

Pre-existing conflicts with main for #5021: core/libs/prx/libSceFiber/Export.cpp, core/libs/prx/libSceLibcInternal/CMakeLists.txt, core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp, core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp, core/libs/prx/libkernel/System/src/SignalSets.cpp, core/libs/tests/GuestMemory.cpp, core/libs/tests/GuestSignalSets.cpp, docs/dev/debt/libkernel.md.

#5018 at 26c62cc63d404d81392f9f4ab004d3a6cd29fe2e was separately normalized and compared with the same main base: clean tree-only pair preview. No combined build is claimed.

RINNECODER added a commit to RINNECODER/AnyPS5 that referenced this pull request Oct 13, 2026
…oykopovar#4501) (#439)

* Port upstream boykopovar#4340 (GNU hash tables) by @ArjenS-LA

Ported from boykopovar/AnyPS5 PR boykopovar#4340 (author @ArjenS-LA), applied as a patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Port upstream boykopovar#4802 (output collisions) by @ArjenS-LA

Ported from boykopovar/AnyPS5 PR boykopovar#4802 (author @ArjenS-LA), applied as a patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Port upstream boykopovar#4501 (SELF skip warning) by @devavijitsingh

Ported from boykopovar/AnyPS5 PR boykopovar#4501 (author @devavijitsingh), applied as a patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Adapt boykopovar#4802 port: hoist registry paths so the collision check runs before registries are written

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@xXPBSXx

xXPBSXx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Windows test: head c6feebf47 merged onto current main 377959777 (clean merge, no conflicts). Windows 10, MinGW GCC (WinLibs), Ninja Release, -j2: full build 0 errors; ctest with the filter ^output_collisions$: output_collisions passed (1 test(s), 0 failed); python tools/check_conventions.py --base upstream/main passed. I did not run the new tests against main without the change.

AI-assisted (Claude Code).

@ArjenS-LA

Copy link
Copy Markdown
Contributor Author

@harry-z-kang: #4916 introduces .needed.json companions that need integration with this PR's output collision preflight in core/relinker/main.cpp.

I checked #4802 at c6feebf47cebe1097f3b6a43b0678bbe72d8889d and #4916 at d92f47c90e9a259f822043c7cb218ef2d9e83780, normalized onto main 88107b7de6cd7c7952c496345ffc4f413bef374b. Git's tree merge is clean, but source inspection of the combined tree shows neededPaths being written without inclusion in outputPaths. An alias between a companion and another generated output would therefore be outside the pairwise preflight.

I propose #4802 first, then including neededPaths in the preflight and extending the output-collision regression when #4916 is rebased. Does that order work with your existing dependencies? If #4916 lands first, this PR will need that integration before merging. No dependency agreement is assumed yet.

This is source inspection and a tree-only merge preview; I have not built or run the combined tree. AI-assisted: OpenAI Codex.

@CnRJay CnRJay 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.

Rejecting outputs that resolve to the same canonical path, including case variants on a case-insensitive directory, is useful: --executable and --module-dir can produce those names. I'm less sure about the hard-link part, which accounts for most of the Windows code.

  • The scenario is rare. It is a hard link that someone created between two future output names before converting. It is worth guarding against, but it costs a lot here: during the preflight the converter creates .anyps5-output-identity-* directories and hard links in the user's output directories, and it refuses read-only outputs with a new error.
  • Windows has a direct identity check. GetFileInformationByHandle on a handle opened with FILE_READ_ATTRIBUTES (and FILE_FLAG_BACKUP_SEMANTICS) gives dwVolumeSerialNumber plus nFileIndexHigh/nFileIndexLow, which identify the file regardless of the libstdc++ permission emulation that breaks equivalent. It needs no write access, so read-only outputs need no special case, and it creates nothing on disk.
  • The case-sensitivity probe could likewise be replaced by comparing the identity of the existing parent directory, or skipped when the names differ only in case and the target doesn't exist yet. Treating that as a collision is the conservative choice and matches the default Windows behaviour.
  • #5018 overlaps. It writes outputs on Linux and macOS to a temporary file and renames it over the target. After that, a hard-linked output is replaced name by name, and the overwrite described here no longer happens on those hosts.

As ArjenS-LA notes, #4916's .needed.json companions also need to go into outputPaths, whichever lands first.

@blawesom

Copy link
Copy Markdown
Contributor

Rebase handoff — rebased onto current main, authorship unchanged.

Three follow-throughs beyond the raw conflict markers: main's ConvertedExecutable/Convert refactor and this PR's OutputIdentityDirectory + pairwise collision loop are both kept (main's per-output checks, then the PR's full collision detection including desktop entries and diagnostics); this PR's Io::ByteWriter uses are renamed to main's Io::FileWriter; and the PR's collision detection now runs against main's refactored output flow.

Verified on Linux (CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON, AMD RX 9070 XT / RADV):

To pick it up:

git fetch https://github.com/blawesom/AnyPS5 rebase/4802
git checkout <your branch>
git merge --ff-only FETCH_HEAD

Head is 1d9394dc3.

@ArjenS-LA
ArjenS-LA force-pushed the fix/relinker-output-collisions branch from c6feebf to 6937530 Compare October 14, 2026 07:02
@ArjenS-LA

Copy link
Copy Markdown
Contributor Author

Rebased and updated at 69375305f500004f5648e683eb012323e6e9ad6b, based on main d8888df9b280a23077ff47d412bd7668c8c8b656.

@blawesom, thanks for the handoff. I retained its integration with ConvertedExecutable/Convert and FileWriter, removed the unused writer allocation, and added every secondary executable to outputPaths. The handoff's loop included only the first executable. The new regression reproduces primary/secondary aliasing returning success before this addition and passes afterward; it also covers a secondary executable colliding directly with windows-diagnostics-imports.txt.

@CnRJay, I checked the design points against current source:

  • The direct Windows identity API would avoid the temporary links, but CONTRIBUTING.md still requires the relinker to use only the C++20 standard library. I retained the existing workaround and read-only guard under that rule; adopting GetFileInformationByHandle needs a maintainer decision on the exception. The case-sensitivity probe preserves acceptance of distinct names in case-sensitive Windows directories, although that acceptance control remains skipped on my local case-insensitive directory.
  • fix(relinker): replace output files instead of rewriting them in place #5018 (26c62cc63d404d81392f9f4ab004d3a6cd29fe2e) reduces hard-link overwrite risk on its successful Linux/macOS replacement path. Windows is unchanged, and the implementation still falls back to in-place writes for symbolic/nonregular outputs or failures to create, prepare, or rename the temporary file. Desktop/icon outputs also remain in-place. I therefore kept collision detection. This is source analysis, not a combined Linux runtime test.
  • fix(relinker): audit dependencies without imported functions #4916 (22b190ae081c5e01119a1f6b3559b64754b18df9) still merges cleanly in the tree preview, but its companions must join the plan before the pairwise loop: outputPaths.insert(outputPaths.end(), neededPaths.begin(), neededPaths.end());. That includes primary, secondary, and module companions. The second PR to land needs that integration and companion collision regression coverage; the dependency-output feature is not present on main yet. No merge-order agreement is assumed.

Validation on Windows x64 / supported WinLibs GCC 15.2.0: both before/after regressions reproduced the intended failures; all 57 hard-link pairs plus direct-path and identity controls pass after the fix. Release relinker build passes; the selected CTest suite has 145 passes, one explicit long-path skip, and no failures. Nine unrelated Git-commit-producing tooling tests were excluded under my local commit limit. Conventions passes on the exact new commit. Native Linux/macOS, full-library, title, and GPU execution were not run locally for this revision.

The refreshed audit covers 433 open PRs and all 27 shared-file candidates at the exact heads in the updated description. Seven pairs are clean; twenty remain inconclusive because the other PR already conflicts with main. No new pairwise conflict was reported. AI-assisted: OpenAI Codex.

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

Copy link
Copy Markdown
Contributor

macOS data point, plus one gap on case-insensitive APFS.

Apple M2 Max, macOS 27.2 (26B5091g), AppleClang, Ninja Release, relinker-only. Head 69375305f merged without conflicts into main 84b01f9cd.

  • arm64: ctest --parallel 8 --timeout 30 158/158 pass. output_collisions passes 25 hard-link pairs and secondary_output_collisions 32; the case-variant section prints Distinct case-variant outputs skipped: case-sensitive directory unavailable (default APFS volume).
  • x86_64 under Rosetta 2 (-DCMAKE_OSX_ARCHITECTURES=x86_64, x86_64 Python): both collision tests pass. 6 other tests fail (pr_overlap_encoding, progress_history, stubgen_stdout_newlines, release_package, linux_fixed_base, guest_module_args), the same 6 that fail on unmodified main on this machine (five are Python 3.9 incompatibilities of the only x86_64 Python here; guest_module_args is the fixture test(relinker): keep the guest_module_args fixture's GOT out of executable memory #5362 fixes).

Gap: the case-variant comparison in SameOutputDestination is under #ifdef _WIN32, but the default macOS volume is case-insensitive too. When neither output exists yet, a collision that differs only in case is not detected. Same command as the test's Windows-only case, in an empty directory:

relinker --windows --windows-diagnostics --skip-sce-module input.elf WINDOWS-DIAGNOSTICS-IMPORTS.TXT
main this PR
Neither file exists rc 0; one file windows-diagnostics-imports.txt holding the 507392-byte PE, diagnostics lost same: rc 0, one file, PE only
WINDOWS-DIAGNOSTICS-IMPORTS.TXT already exists rc 0, overwritten rc 2, Output paths collide, previous file kept

So the PR catches the existing-file case on macOS (via equivalent), but not the fresh one. The Windows probe (OutputIdentityDirectory plus an upper-cased lookup) would also work on macOS if it were not limited to _WIN32. Alternatively, pathconf(dir, _PC_CASE_SENSITIVE) reports this on macOS.

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

@CnRJay

CnRJay commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

You're right, I missed that rule. CONTRIBUTING says "The relinker uses only the C++20 standard library", so GetFileInformationByHandle isn't an option without a maintainer exception, and I withdraw that suggestion. Within the standard library, the temporary-link comparison is a reasonable way around libstdc++'s equivalent on MinGW. Your point about #5018's fallbacks (symlinks, non-regular outputs, failed temporary creation, and the desktop/icon files written in place) also holds, so keeping the collision check makes sense.

@adriadam10 adriadam10 self-assigned this Oct 14, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the collision preflight against main: with core/relinker/main.cpp reverted, output_collisions and secondary_output_collisions both fail; with the PR they pass. Relinker build + ctest (131 tests) green on Linux, relinker CI green on Linux/Windows/macOS, and the Windows/macOS runs reported in the thread agree. Merging.

Follow-ups outside this PR: a fresh case-only collision on case-insensitive macOS volumes (reported above) and .needed.json companions once #4916 lands.

@adriadam10
adriadam10 merged commit 2f3adfb into boykopovar:main Oct 14, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants