Skip to content

fix(libSceSystemService): pass LoadExec arguments as the whole guest argv - #5083

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
Dripl1x:codex/secondary-loadexec
Oct 14, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
Dripl1x:codex/secondary-loadexec

Conversation

@Dripl1x

@Dripl1x Dripl1x commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

sceSystemServiceLoadExec with a nonempty argument list now starts the new executable with that list as its whole guest argv, with no program name in front. main passes the list after the host path, so the guest sees the host path as argv[0] and every argument one position later.

This follows the published PS5 RetroArch console transcript: a restart with one argument enters with argc=1, argv[0]="--ps5-relaunch=1", and a launch from the home screen with argc=1, argv[0]="" (its caller). It is published console evidence, not a console run performed for this PR.

  • Linux checks that the converted target is an executable x86-64 ELF, runs the shutdown callbacks, makes the target's directory the working directory, closes the old descriptors except the standard streams and replaces the process with fexecve. The PID is kept.
  • Windows starts a suspended process that inherits only the standard streams and a handle of the old process. Its generated entry stub waits for the old process to end before it loads the title's libraries; the stub and the kernel argument getters drop the native command-line prefix together.
  • The old process ends after its shutdown callbacks, without running guest exit handlers or host destructors, as the existing launch path in main does.
  • A null list or one without a first entry keeps the existing launch path. What the console passes for such a list, its argument limits and its error codes are unknown and recorded in technical debt. macOS keeps the existing path.

Title

RetroArch (PPSA99169) needs this when a game returns to ES-DE. YossiYad ran the whole frontend chain on Linux (comment). The title was converted with --executable picker/picker.bin --executable es-de/es-de.bin and started in its game mode, with nova.nes (GPL NES homebrew) and ES-DE as the frontend. After the game quits, RetroArch starts ES-DE through sceSystemServiceLoadExec.

Related:

Tested

Windows 11, Ryzen 7 5800H, WinLibs MinGW-w64 GCC 15.2.0 r7; Ubuntu 26.04 under WSL, GCC 15.2.0. Release, BUILD_TESTING=ON, the root configuration with the bundled dependency sources; FFMPEG_PREBUILT_DIR pointed at an already unpacked package. Head a1b80121c on main 84b01f9cd.

  • Built relinker, libc, libkernel, libSceAgcDriver, libSceSystemService, the fixture library and the test targets. Then ran loadexec_arguments, guest_system_exit_parameters, guest_system_exit, guest_quick_exit, guest_immediate_exit, guest_return_from_main, guest_system_relaunch, guest_nested_exit, relinker_conversion and relinker_executables, and on Windows also guest_system_service_path_boundary. 11 of 11 pass on Windows (3.47 s) and the 10 that exist on Linux pass (1.13 s). loadexec_arguments and guest_system_relaunch also passed 10 repeated runs on Windows.
  • loadexec_arguments converts two synthetic 0xFE10 images with the actual relinker (--executable picker/second.bin) and runs first -> second -> first against the built libraries, under a work directory with Cyrillic and CJK characters. It checks:
    • the entry argument block and getargc/getargv for an empty first argument, spaces, quotes with trailing backslashes, Cyrillic and CJK;
    • a fresh runtime state, the process IDs (three on Windows, one on Linux) and the working directory;
    • a guest /app0 resource, and that only the shutdown callbacks run before the handover;
    • that absent, outside-/app0, .. and unconverted targets are rejected before shutdown.
  • Negative control: with Export.cpp from main and the new test, loadexec_arguments fails on both systems with LoadExec fixture: entry argument count.
  • The conventions checker, including the title and description, and git diff --check pass.

Not tested: a console, macOS, the full test suite. RetroArch was run by YossiYad on Linux as quoted above, not for this rebased head. Full PR builds need a collaborator's manual Build run.

Checklist

🤖 Generated with Claude Code

@xXPBSXx

xXPBSXx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Review note (static): git merge-tree of the current head against main reports four conflicts: core/libs/prx/libSceSystemService/Export.cpp, core/libs/tests/GuestSystemExit.cpp, core/relinker/main.cpp and docs/dev/debt/libSceSystemService.md. The description says the stack includes the prerequisite commits of #4872 and #4786 (the branch has three commits, the first from #4872), so it also overlaps those open pull requests and needs a rebase onto current main before it can be reviewed in full. Smaller point: the description reports 15/15 CTests on Windows and in WSL, but also that RetroArch itself has not been run and that the automatic conversion policy needs a review; it might be easier to review if the relinker discovery (secondary executables) and the runtime replacement were separate pull requests.

AI-assisted (Claude Code).

@Dripl1x
Dripl1x force-pushed the codex/secondary-loadexec branch from a02109e to f2e8681 Compare October 13, 2026 11:45
@Dripl1x Dripl1x changed the title feat(libSceSystemService): replace title executables through LoadExec feat(relinker): discover executables and preserve LoadExec arguments Oct 13, 2026
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@AaravMehta-07

Copy link
Copy Markdown
Contributor

Source review found a Windows-only path-boundary gap in the current LoadExec path: ConvertedExecutableName rejects /-delimited .. components, but Windows also treats backslashes as separators. A guest path such as /app0/..\..\outside can therefore reach PrepareLaunch as a single component and resolve outside the executable directory. This PR's current refactor leaves that conversion/containment path unchanged.

I am holding a separate containment fix because it overlaps this refactor. Could your refactor go first, with the narrow guard and exported-API regression test rebased afterward? This is a source finding only; I have not reproduced it in the guest runtime yet.

The investigation, proposed patch and this comment are AI-assisted.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@AaravMehta-07

AaravMehta-07 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Runtime follow-up: the path-boundary case is now reproduced through the actual Windows sceSystemServiceLoadExec export. A probe using a benign copy of its own test executable outside the sibling directory reaches a registered shutdown callback on main. That callback exits with code 86 before StartHostExecutable, so the test does not launch an outside executable. With the parent-directory guard, the probe throws before shutdown and all five focused SystemService/SystemExit tests pass (0.56 seconds total).

Exact overlap follow-up: the complete live hunk/export/new-file scanner reports no overlap, including with #5083. Your current diff changes the outer LoadExec body but leaves PrepareLaunch unchanged; this guard changes the path-preparation helpers. The initial broad file-level concern was therefore cleared by the more precise check. The independently tested guard is submitted as #5158, with this distinction documented and no semantic dependency on your refactor. I will rebase if future edits introduce a conflict.

The patch and test are AI-assisted; no commercial-title or PS5 hardware result is claimed.

@Dripl1x

Dripl1x commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Updated to 9d4464a0c, rebased onto main 8cf6a0bc3. What changed since the review notes above:

Neither #5236 nor #5158 is a prerequisite of this PR. I suggest they go first, since they are smaller, and I rebase this one after either merges; say so if you prefer another order.

Windows 11, WinLibs MinGW-w64 GCC 15.2.0 r7; Ubuntu 26.04 under WSL, GCC 15.2.0; Release, BUILD_TESTING=ON, root configuration. Nine tests pass on both systems. With Export.cpp from main and the new test, loadexec_arguments fails on both with LoadExec fixture: entry argument count. Local paths in the logs are replaced with <build> and <source>.

Windows with the change (9 of 9)
Test project <build>
    Start  800: loadexec_arguments
1/9 Test  #800: loadexec_arguments ...............   Passed    1.61 sec
    Start  801: guest_system_exit_parameters
2/9 Test  #801: guest_system_exit_parameters .....   Passed    0.24 sec
    Start  802: guest_system_exit
3/9 Test  #802: guest_system_exit ................   Passed    0.03 sec
    Start  803: guest_quick_exit
4/9 Test  #803: guest_quick_exit .................   Passed    0.02 sec
    Start  804: guest_immediate_exit
5/9 Test  #804: guest_immediate_exit .............   Passed    0.04 sec
    Start  805: guest_return_from_main
6/9 Test  #805: guest_return_from_main ...........   Passed    0.03 sec
    Start  806: guest_system_relaunch
7/9 Test  #806: guest_system_relaunch ............   Passed    0.23 sec
    Start  978: relinker_conversion
8/9 Test  #978: relinker_conversion ..............   Passed    0.35 sec
    Start 1060: relinker_executables
9/9 Test #1060: relinker_executables .............   Passed    1.23 sec

100% tests passed, 0 tests failed out of 9

Total Test time (real) =   3.95 sec
Windows control: Export.cpp from main, new test
Test project <build>
    Start 800: loadexec_arguments
1/2 Test #800: loadexec_arguments ...............***Failed    0.80 sec
Traceback (most recent call last):
  File "<source>\core\relinker\relinker\tests\test_loadexec_arguments.py", line 92, in <module>
    main()
    ~~~~^^
  File "<source>\core\relinker\relinker\tests\test_loadexec_arguments.py", line 83, in main
    assert events == 'first\nshutdown\nsecond\nshutdown\nreturned\n', (run.returncode, events, run.stdout, run.stderr)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: (0, 'first\nshutdown\n', b'Transferring control to ELF entry point\nTransferring control to ELF entry point\n', b'[time] scheduler tick raised to 0.5 ms (was 0.50 ms, now 0.50 ms, timed waits fine)\r\n[time] scheduler tick raised to 0.5 ms (was 0.50 ms, now 0.50 ms, timed waits fine)\r\nLoadExec fixture: entry argument count\r\n')

    Start 806: guest_system_relaunch
2/2 Test #806: guest_system_relaunch ............   Passed    0.19 sec

50% tests passed, 1 tests failed out of 2

Total Test time (real) =   1.13 sec

The following tests FAILED:
	800 - loadexec_arguments (Failed)
Errors while running CTest
Linux with the change (9 of 9)
Test project <build>
    Start  769: loadexec_arguments
1/9 Test  #769: loadexec_arguments ...............   Passed    0.56 sec
    Start  770: guest_system_exit_parameters
2/9 Test  #770: guest_system_exit_parameters .....   Passed    0.01 sec
    Start  771: guest_system_exit
3/9 Test  #771: guest_system_exit ................   Passed    0.01 sec
    Start  772: guest_quick_exit
4/9 Test  #772: guest_quick_exit .................   Passed    0.01 sec
    Start  773: guest_immediate_exit
5/9 Test  #773: guest_immediate_exit .............   Passed    0.01 sec
    Start  774: guest_return_from_main
6/9 Test  #774: guest_return_from_main ...........   Passed    0.01 sec
    Start  775: guest_system_relaunch
7/9 Test  #775: guest_system_relaunch ............   Passed    0.07 sec
    Start  947: relinker_conversion
8/9 Test  #947: relinker_conversion ..............   Passed    0.07 sec
    Start 1029: relinker_executables
9/9 Test #1029: relinker_executables .............   Passed    0.41 sec

100% tests passed, 0 tests failed out of 9

Total Test time (real) =   1.19 sec
Linux control: Export.cpp from main, new test
Test project <build>
    Start  769: loadexec_arguments
1/9 Test  #769: loadexec_arguments ...............***Failed    0.71 sec
Traceback (most recent call last):
  File "<source>/core/relinker/relinker/tests/test_loadexec_arguments.py", line 92, in <module>
    main()
    ~~~~^^
  File "<source>/core/relinker/relinker/tests/test_loadexec_arguments.py", line 83, in main
    assert events == 'first\nshutdown\nsecond\nshutdown\nreturned\n', (run.returncode, events, run.stdout, run.stderr)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: (98, 'first\nshutdown\n', b'', b'LoadExec fixture: entry argument count\n')

    Start  770: guest_system_exit_parameters
2/9 Test  #770: guest_system_exit_parameters .....   Passed    0.01 sec
    Start  771: guest_system_exit
3/9 Test  #771: guest_system_exit ................   Passed    0.01 sec
    Start  772: guest_quick_exit
4/9 Test  #772: guest_quick_exit .................   Passed    0.01 sec
    Start  773: guest_immediate_exit
5/9 Test  #773: guest_immediate_exit .............   Passed    0.01 sec
    Start  774: guest_return_from_main
6/9 Test  #774: guest_return_from_main ...........   Passed    0.01 sec
    Start  775: guest_system_relaunch
7/9 Test  #775: guest_system_relaunch ............   Passed    0.07 sec
    Start  947: relinker_conversion
8/9 Test  #947: relinker_conversion ..............   Passed    0.08 sec
    Start 1029: relinker_executables
9/9 Test #1029: relinker_executables .............   Passed    0.35 sec

89% tests passed, 1 tests failed out of 9

Total Test time (real) =   1.27 sec

The following tests FAILED:
	769 - loadexec_arguments (Failed)
Errors while running CTest

AI-assisted (Claude Code).

@Dripl1x Dripl1x changed the title feat(relinker): discover executables and preserve LoadExec arguments fix(libSceSystemService): pass LoadExec arguments as the whole guest argv Oct 13, 2026
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 13, 2026
@Dripl1x

Dripl1x commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

The PR overlap report now lists #5158, #5236 and #5284 against this PR, so the checklist declares the order proposed above: Depends on: #5158, #5236, #5284. None is a functional prerequisite; each conflicts on adjacent lines only, and this PR is rebased after each of them merges.

@MarcZX100 for #5284 (37ea3b651): a trial git merge-tree conflicts only in core/libs/tests/GuestSystemExit.cpp, where your --nested-exit-driver and --nested-exit modes sit next to the mode checks this PR changes. The resolution keeps both; in the rebased version your two checks read argc > firstArgument and argv[firstArgument] like the others.

AI-assisted (Claude Code).

@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for the separate merge-tree check and confirming the conflict keeps both changes. The ordering you declared works for me: #5284 is adjacent test-mode coverage rather than a functional prerequisite, and I can rebase it if #5083 lands first.

@Dripl1x

Dripl1x commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Which titles pass an argument vector to sceSystemServiceLoadExec, from a static survey of the homebrew catalog (no title was run):

  • 44 of the 63 executables import sceSystemServiceLoadExec; 37 have the call ("exit", NULL).
  • Four have a call site whose second argument is the address of an array on the stack, the case this PR changes:
    • RetroArch (PPSA99169): six call sites, one of them with the path /app0/eboot.bin;
    • Prospero Win (PPSA99995): three call sites, with /app0/eboot.bin and /mnt/sandbox/PPSA99995_000/app0/eboot.bin;
    • Middle Ages Ping Pong (PPSA28319): one call site;
    • PSBox Cloud Gaming (PPSA99810): one call site, with /app0/eboot.bin and a vector that holds only the terminating null.
  • What the vectors contain at run time is not recovered.

Method: the 65 release archives listed in ps5-homebrew-catalog were downloaded (SHA-256 equal to the catalog records), 63 of them hold an eboot.bin, which was unwrapped from its fake-signed container. Nothing was run. Imports come from relinker --registry and tools/import_audit.py on main bfc10637d. Call arguments are read from the instructions before each call site (objdump), which recovers a value only where the compiler loads a constant right before the call, so every count below is a lower bound.

AI-assisted (Claude Code).

@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for adding this survey. It gives useful evidence that the array-address call pattern occurs in real homebrew binaries, including explicit /app0/eboot.bin paths. As you note, the static call-site scan is a lower bound and does not recover the vector contents at runtime, so I read it as evidence of usage rather than a title-level runtime result. The current host regression and control still demonstrate the argument-vector behavior itself.

AI-assisted: yes; reviewed by MarcZX100.

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

Copy link
Copy Markdown
Contributor

Linux run of the full RetroArch frontend chain this PR is about: RetroArch (PPSA99169, catalog release) converted with --executable picker/picker.bin --executable es-de/es-de.bin, Bazzite 44, GCC 16.2, Release, RX 7700 XT (RADV). Head 9d4464a0c merged into main 315260903.

The run uses the title's game mode, the way ES-DE starts a game: app0/game-request.txt names nova.nes (Nova the Squirrel, GPL NES homebrew) with fceumm and /app0/es-de/es-de.bin as the frontend, args.txt holds --max-frames=600, and the title starts with --ps5-mode=game. RetroArch runs the game, quits with status 0 after 11 s, and starts ES-DE through sceSystemServiceLoadExec.

Merging into current main conflicts in core/libs/tests/GuestSystemExit.cpp and core/libs/prx/libSceSystemService/CMakeLists.txt. For this run I took this PR's side of both; the runtime build had no tests.

One interaction with #5699, which I opened: after a guest dup2 onto descriptor 2 (RetroArch points stderr at app0/trace.txt), the host standard error #4792 keeps is close-on-exec, so the next executable's runtime output goes to the title's file. In this run ES-DE's terminate report landed in trace.txt, not on the console. #5699 puts the kept descriptor back on 2 before execv. Replace here keeps descriptors 0 to 2 as they are and calls fexecve, so it needs the same dup2 before close_range. Whichever of the two lands second can add it; I can update #5699 if this one goes in first.

…argv

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Dripl1x
Dripl1x force-pushed the codex/secondary-loadexec branch from 9d4464a to a1b8012 Compare October 14, 2026 15:52
@Dripl1x

Dripl1x commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@YossiYad thank you for the run. It is the title evidence this PR lacked: on main, ES-DE started by RetroArch after a game takes the host path in argv[0] for an option and returns at once, and with this PR it starts. I've quoted it in the description.

Rebased onto main 84b01f9cd, now a1b80121c:

  • GuestSystemExit.cpp keeps fix(libc): finish exit handlers after nested exit #5284's nested-exit modes, written in the firstArgument form of the other modes. libSceSystemService/CMakeLists.txt keeps fix(libSceSystemService): contain converted executable paths #5158's path-boundary test next to loadexec_arguments.
  • libSceSystemService now links libSceAgcDriver on main, so loadexec_arguments also copies that library beside the converted fixture. Without it the converted fixture failed to load on Linux.
  • Windows: 11 of 11 pass. Linux: the 10 that exist there pass. With main's Export.cpp, loadexec_arguments still fails on both with LoadExec fixture: entry argument count.

On #5699: agreed. Whichever lands second adds the dup2 of the kept host standard error before close_range in Replace. If #5699 is merged first, I'll add it here with your test covering it.

AI-assisted (Claude Code).

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
@adriadam10 adriadam10 self-assigned this Oct 14, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on Linux with the PR merged onto current main: build and tests green (312/312 in scope), including loadexec_arguments, guest_system_exit*, guest_system_relaunch and guest_nested_exit. With Export.cpp reverted to main, loadexec_arguments fails, so the test covers the change. The Windows entry stub and CreateProcessW path are covered by loadexec_arguments on the dispatched Windows Build; merging once that is green. Thanks @YossiYad for the title run.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
#5978 core/libs/tests/GuestSystemExit.cpp L171-172

@adriadam10
adriadam10 merged commit 358e218 into boykopovar:main Oct 14, 2026
10 of 11 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