Skip to content

fix(libkernel): provide default arguments block for DT_INIT on Linux - #6149

Open
Nubi-Prey wants to merge 1 commit into
boykopovar:mainfrom
Nubi-Prey:fix/libkernel-default-module-args
Open

Nubi-Prey wants to merge 1 commit into
boykopovar:mainfrom
Nubi-Prey:fix/libkernel-default-module-args

Conversation

@Nubi-Prey

@Nubi-Prey Nubi-Prey commented Oct 14, 2026 •

Copy link
Copy Markdown

What

  • Extract pending guest module argument state into core/libs/prx/libkernel/Module/src/ModuleArgs.cpp and ModuleArgs.hpp (SetPendingModuleArgs_nid_no_patch, GetPendingModuleInitResult_nid_no_patch, SetModuleInitResultObserverForTest_nid_no_patch) and link it into both libkernel and guest_module_args_host so the relinker test exercises the production implementation directly.
  • Track whether an explicit sceKernelLoadStartModule call is active via thread_local bool explicitLoadInProgress (with exception-safe reset around dlopen_nid_postfix in Module.cpp), preserving explicit (args, nullptr) pairs when explicitLoadInProgress is true and returning a 16-byte default block {size: 0x10, version: 0x200, context: nullptr} (args = 16) only when explicitLoadInProgress is false.
  • Document the unmeasured console semantics in docs/dev/debt/libkernel.md: what the console passes outside sceKernelLoadStartModule or to DT_NEEDED dependencies pulled in during sceKernelLoadStartModule, that {0x10, 0x200} is a guess from Linux: Cult of the Lamb (PPSA06464) faults at process start in UnifyPS5Plugin.prx, a needed library started with a null arguments block #5621 while context is unknown (nullptr faults if called during init), and that explicitLoadInProgress remains true across dlopen_nid_postfix.

Tested

  • Linux x86_64 (-DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON), rebased on main (474030e584): built relinker and guest_module_args_host and ran ctest --test-dir build-relinker -R "guest_module_args|guest_deferred_dependencies" --output-on-failure -V (2/2 passed: load_and_record, load_and_record_default, load_and_record_explicit_null, and guest_deferred_dependencies).
  • Negative controls verified and reverted:
    1. Mutating SceKernelDefaultModuleArgs::version to 0x201 in ModuleArgs.hpp fails guest_module_args with Guest module argument fixture: Default arguments corruption.
    2. Falling back to kEmptyPendingArgs when pendingModuleArgs.argp == nullptr in ModuleArgs.cpp fails load_and_record_explicit_null in guest_module_args.
  • python3 tools/check_conventions.py --base upstream/main passes with 0 warnings and 0 errors.
  • Not tested with a commercial title in this environment.

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
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes (Gemini 4 Argon)

@simonbbby

Copy link
Copy Markdown
Contributor

Independent Linux check at exact head 950bc7c323a44bac2f83781e57d968cf016e932f (Ubuntu 24.04 / Ryzen 9 9950X / GCC 13.3 / Ninja Release).

The relinker-only suite passes 165/165 (28.88 s), and the real production getter returns the proposed default block in a separate native probe. There is a gap in the new regression, though: it remains green with the runtime fix removed, and also with a null default pointer in its own provider.

Controls, rebuilt and run with ctest -R '^guest_module_args$' -V:

Change from the stated head Result
None pass
Restore only production Module.cpp from this head's parent pass
Provider kEmptyPending{sizeof(kDefaultArgs), nullptr} pass
Provider kEmptyPending{8, &kDefaultArgs} fail: the Python size assertion
Null-pointer provider plus the assertion change below fail: Missing default argument block
Correct provider plus the assertion change below pass

The size mutation confirms the fixture ran and rebuilt. The two false greens have separate causes: guest_module_args_host.prx supplies its own __aps5_get_pending_module_args_nid_no_patch, so it never calls the changed production helper; and RecordArgs checks the payload only if argp == &kDefaultArgs. A null pointer skips that check, while Python asserts the size and initializer result but not the pointer. The fixture's initializer only calls RecordArgs, so it does not dereference the null pointer as the reported Unity initializer does.

This small test-only change catches the null pointer and also checks context:

--- a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp
+++ b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp
@@ -48,9 +48,10 @@
     std::snprintf(line, sizeof(line), "args=%016zx argp=%016zx\n", args,
                   static_cast<std::size_t>(reinterpret_cast<std::uintptr_t>(argp)));
     Write(line);
-    if (args == sizeof(DefaultModuleArgs) && argp == &kDefaultArgs) {
+    if (args == sizeof(DefaultModuleArgs)) {
+        if (argp == nullptr) Fail("Missing default argument block");
         const auto* defaultArgs = static_cast<const DefaultModuleArgs*>(argp);
-        if (defaultArgs->size != 0x10 || defaultArgs->version != 0x200) Fail("Default arguments corruption");
+        if (defaultArgs->size != 0x10 || defaultArgs->version != 0x200 || defaultArgs->context != nullptr) Fail("Default arguments corruption");
     }
 }
 

It still needs a regression that calls the real runtime getter. I checked that independently without the full SDL build: compile the actual core/libs/prx/libkernel/Module/src/Module.cpp with -std=c++20 -O2 -ffunction-sections -fdata-sections -Icore/libs, then link a small getter probe with -Wl,--gc-sections to discard unrelated unused runtime functions. The probe casts the getter result to PendingModuleArgs, requires args == 16 and non-null argp, then checks the payload's size 0x10, version 0x200 and null context. The PR production object passes; its parent's production object exits 1 with a missing/null default block. This links the actual compiled helper, rather than a replacement getter.

All temporary source edits were restored, the stock provider rebuilt, and guest_module_args passes again with a clean checkout. No title run, full-runtime load, or claim that this default block matches console behavior.

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

macOS run of the new --load-default path, which has its own darwin branch (RTLD_GLOBAL) that the Linux runs don't cover.

Machine: Apple M2 Max, macOS 27.2, Xcode 27 toolchain, relinker-only Release.
Head: 950bc7c32, merged onto main @ ab9a47684 (clean).

The test only executes natively on macOS when the interpreter is x86_64 (platform.machine() == 'x86_64'). Under ctest on Apple silicon, both the arm64 build and the x86_64 build print native execution skipped, because /usr/bin/python3 starts as arm64. So I ran it by hand from the x86_64 build:

arch -x86_64 /usr/bin/python3 core/relinker/relinker/tests/test_guest_module_args.py <build>/core/relinker/relinker <build>/core/relinker/guest_module_args_host.prx
Change Result (x86_64 under Rosetta, Python 3.9.6)
None pass
Fixture provider GuestModuleArgsHost.cpp restored from main fail: args=0000000000000000 argp=0000000000000000
--load-default with DEFAULT_MODE instead of RTLD_GLOBAL fail: dlopen(...): symbol not found in flat namespace '_RecordArgs'

So the darwin branch works and the RTLD_GLOBAL is required there. As @simonbbby showed on Linux, what the test checks is the fixture's own provider, not the production __aps5_get_pending_module_args_nid_no_patch in Module.cpp. That is the same on macOS: Module.cpp is not part of the relinker-only build.

Full relinker-only suite with the PR: arm64 163/163; x86_64 + Python 3.9.6 159/163, with the same 4 failures as main.

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

@Nubi-Prey

Nubi-Prey commented Oct 15, 2026 •

Copy link
Copy Markdown
Author

Thanks @jotitadev and @simonbbby for the thorough independent validation on macOS and Linux!

Carried the suggested mutation control in commit 20e36060f:

  • In GuestModuleArgsHost.cpp: fail with "Missing default argument block" if argp == nullptr when args == sizeof(DefaultModuleArgs), and check context == nullptr.
  • In test_guest_module_args.py: assert that the recorded argp is not null.

Verified locally with a negative mutation control (setting nullptr fails CTest immediately with the expected error) and all 165 CTests and conventions pass on a clean checkout.

Written with AI assistance (Gemini).

This was referenced Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 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
PR 7532 core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp L54-63

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 4563 core/libs/prx/libkernel/CMakeLists.txt
PR 4630 docs/dev/debt/libkernel.md
PR 5021 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Module/src/Module.cpp
core/relinker/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 6740 docs/dev/debt/libkernel.md
PR 6936 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7088 docs/dev/debt/libkernel.md
PR 7105 docs/dev/debt/libkernel.md
PR 7121 docs/dev/debt/libkernel.md
PR 7135 docs/dev/debt/libkernel.md
PR 7149 docs/dev/debt/libkernel.md
PR 7192 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7447 docs/dev/debt/libkernel.md
PR 7464 docs/dev/debt/libkernel.md
PR 7531 core/relinker/CMakeLists.txt
PR 7804 docs/dev/debt/libkernel.md
PR 9019 core/libs/prx/libkernel/CMakeLists.txt
PR 9156 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9199 docs/dev/debt/libkernel.md
PR 9293 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9297 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9302 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9383 core/libs/prx/libkernel/CMakeLists.txt
PR 9389 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9557 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9566 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9843 docs/dev/debt/libkernel.md
PR 9938 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10025 docs/dev/debt/libkernel.md
PR 10319 docs/dev/debt/libkernel.md
PR 10442 docs/dev/debt/libkernel.md
PR 10861 core/relinker/CMakeLists.txt
PR 11831 core/libs/prx/libkernel/CMakeLists.txt
PR 11896 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11939 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 12104 docs/dev/debt/libkernel.md
PR 12544 core/libs/prx/libkernel/CMakeLists.txt
PR 12560 docs/dev/debt/libkernel.md
PR 12678 core/relinker/CMakeLists.txt
PR 12682 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 12688 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 12700 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 12965 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 12970 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 13560 docs/dev/debt/libkernel.md
PR 14103 docs/dev/debt/libkernel.md
PR 14128 docs/dev/debt/libkernel.md
PR 14212 docs/dev/debt/libkernel.md
PR 14264 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 14299 core/libs/prx/libkernel/CMakeLists.txt
PR 14310 docs/dev/debt/libkernel.md
PR 14832 docs/dev/debt/libkernel.md
PR 14854 docs/dev/debt/libkernel.md
PR 14886 docs/dev/debt/libkernel.md
PR 14894 docs/dev/debt/libkernel.md

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@Nubi-Prey This PR now conflicts with main in core/relinker/relinker/tests/test_guest_module_args.py, docs/dev/debt/libkernel.md. Main’s macOS loader setup changed the test call that this PR extends with default module arguments. Could you reconcile the default-argument fixture with the current loader setup and platform behavior?

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

Reviewed — changes requested

The head conflicts with current main. The fallback also replaces the arguments of explicit sceKernelLoadStartModule calls, not only DT_NEEDED initializers. The new regression still passes when the production change is reverted.

What I ran

  • PR head: 0cd9877c6d3224c6207ed58948bc102330350648
  • main: 2960e9a6e7e1fc99c4ed180c71495b5eb83464c7
  • Machine: Arch Linux 7.2.11-zen, GCC 16.2.1, Intel Core i9-14900KF, NVIDIA GeForce RTX 4070 Ti SUPER (driver 615.78.08), in a clean worktree with a Release/Ninja build.
git fetch -q origin main pull/6149/head:refs/review/6149
git checkout -q --detach origin/main
git merge -q --no-ff --no-edit refs/review/6149   # conflicts, aborted
git checkout -q --detach refs/review/6149
git diff --check origin/main...refs/review/6149
python3 tools/check_conventions.py --base origin/main
cmake --build build -j10 --target relinker guest_module_args_host
ctest --test-dir build -R '^guest_module_args$' --output-on-failure
cmake --build build -j10 --target libs
# negative control
git checkout 92e50418f33b8950ceea950cb37331a941f5be0e -- core/libs/prx/libkernel/Module/src/Module.cpp
cmake --build build -j10 --target libs relinker guest_module_args_host
ctest --test-dir build -R '^guest_module_args$' --output-on-failure
git checkout refs/review/6149 -- core/libs/prx/libkernel/Module/src/Module.cpp

Results

Check Result
Merge onto main Conflict in core/relinker/relinker/tests/test_guest_module_args.py and docs/dev/debt/libkernel.md
git diff --check clean
tools/check_conventions.py --base origin/main passed
Build relinker, guest_module_args_host (PR head) passed
Build libs (PR head) passed
guest_module_args (PR head) passed, 0.11 s
guest_module_args with production Module.cpp reverted passed, 0.11 s
Merge conflict
CONFLIT (contenu) : Conflit de fusion dans core/relinker/relinker/tests/test_guest_module_args.py
CONFLIT (contenu) : Conflit de fusion dans docs/dev/debt/libkernel.md

On main, the --load branch of the test now drives __aps5_guest_lifecycle_nid_no_patch itself, and the Linux setup copies the provider into libs/libkernel.prx. The PR's --load-default branch and its load_and_record_default calls were written against the earlier version of that code.

Negative control
1/1 Test #1224: guest_module_args ................   Passed    0.11 sec
100% tests passed out of 1

Negative control

I restored only core/libs/prx/libkernel/Module/src/Module.cpp from the PR's merge base 92e50418f3, then rebuilt libs, relinker and guest_module_args_host. guest_module_args still passes. I used the merge base rather than main because the PR head does not merge with main. This confirms, on the current head, what @simonbbby reported for 950bc7c32. The later null-pointer check in the fixture catches a null pointer in the fixture's own provider. It does not make the test exercise the production getter.

Findings

Blocking

  1. Conflicts with main. The PR conflicts in core/relinker/relinker/tests/test_guest_module_args.py and docs/dev/debt/libkernel.md. @oneandonlydean flagged this on 2026-10-25, and the head has not changed since. main has also changed the code this PR depends on. Linux explicit runtime directories now use deferred initialization, which reads the getter in InitializeLinuxGuestModule, and the debt doc has a Linux runtime module initialization entry. After the rebase, the claim that a DT_NEEDED module runs its entry at process start before any sceKernelLoadStartModule should be checked again against the deferred path.

  2. The fallback rewrites explicit load arguments, not only DT_NEEDED initializers. Module.cpp:276-281 substitutes {0x10, &kDefaultModuleArgs} whenever argp == nullptr. However, sceKernelLoadStartModule stores the caller's {args, argp} verbatim at line 294. @oneandonlydean raised the args == 0 case. One more input changes as well: an explicit sceKernelLoadStartModule(path, 8, nullptr, ...) reaches the module entry as (0x10, &kDefaultModuleArgs), so the caller's args value is replaced as well as argp. This is a value the title did not pass, and its console behaviour is unverified. The same happens for a guest dlopen on a thread without pending arguments, because pendingModuleArgs is thread_local.
    Suggested fix: record whether an explicit start is in flight, for example a separate thread-local flag set and cleared around dlopen_nid_postfix in sceKernelLoadStartModule. The default block should apply only when no explicit start is in progress. Explicit loads would then always pass the caller's arguments unchanged. Update the debt entry to say exactly which loads receive the block.

  3. The regression does not cover the production change. The fixture provider GuestModuleArgsHost.cpp:65-70 contains its own copy of the fallback, so --load-default checks the test's copy. The negative control above passes with the production fix reverted. Two options would fix this. A test could call the real __aps5_get_pending_module_args_nid_no_patch from Module.cpp: @simonbbby and @JJenkx both did this outside CTest, with a probe and with ctypes. Or the fixture could forward to the production object instead of reimplementing it. Either test should also cover the explicit-load case from finding 2, checking that (8, nullptr) reaches the entry unchanged.

Non-blocking

  1. Scope of the description. "Closes #5621" and "Unblocks: PPSA06464 #5621" are claimed, but the description does not say whether Cult of the Lamb ran past UnifyPS5Plugin.prx initialization on this head. @MarcZX10 and @oneandonlydean asked this, and it is still unanswered. @JJenkx's PPSA16018 run and @socket1001's analysis show a plugin that calls context as a function and crashes on the null value. I did not run a title, so I cannot add evidence either way. The description should say what was reproduced. The debt entry could also note that some plugins dereference context, because a later reader would otherwise assume that a null context is safe.

Not verified

  • No title was run. I have no evidence on whether PPSA06464 gets past the initializer.
  • I did not probe the production getter at runtime myself. I relied on code reading, and on the probes @simonbbby and @JJenkx describe, which show it returns the 16-byte block.
  • The merged tree was not built or tested because of the conflict. Everything above ran on the PR head, which is based on 92e50418f3.
  • I did not run macOS or Windows.

To get this to a good state

  1. Rebase onto current main and resolve the test and debt-doc conflicts against the deferred lifecycle path.
  2. Limit the default block to initializers that run without an explicit sceKernelLoadStartModule. Explicit loads should pass the caller's {args, argp} unchanged, including (0, nullptr) and (N, nullptr).
  3. Make the regression exercise the production getter, and show that it fails with the Module.cpp change reverted.
  4. Scope Unblocks and the debt entry to what was reproduced on the current head.

@Nubi-Prey
Nubi-Prey force-pushed the fix/libkernel-default-module-args branch from 0cd9877 to d8be19d Compare October 30, 2026 00:26
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 30, 2026
@Nubi-Prey

Copy link
Copy Markdown
Author

Thank you @XairRrR, @simonbbby, and @oneandonlydean for the detailed review and guidance. All points have been addressed in the rebased commit:

  1. Rebase: Rebased cleanly onto latest main, resolving the test and debt-doc conflicts against the deferred lifecycle changes.
  2. Preserve explicit load arguments: Added thread-local explicitLoadInProgress tracking in ModuleArgs.cpp set and cleared across dlopen_nid_postfix in sceKernelLoadStartModule. The default 16-byte block is now only delivered when no explicit load is active. Explicit arguments such as (0, nullptr) or (8, nullptr) are passed through to the module untouched.
  3. Connect test fixture to production TU: Extracted pending module arguments state and getter/setter into a dedicated translation unit (core/libs/prx/libkernel/Module/src/ModuleArgs.cpp), which is now compiled directly into both libkernel and guest_module_args_host.prx. The mock getter in the fixture has been removed.
  4. Regression & negative controls: Extended test_guest_module_args.py with load_and_record_explicit_null checking (8, nullptr). Verified with mutation controls: reverting ModuleArgs.cpp or restoring the old argp == nullptr check fails the CTest suite immediately.
  5. Technical debt & unblocks: Updated docs/dev/debt/libkernel.md stating the unverified console gap first, clarifying the DT_INIT vs explicit load distinction, and noting that subsequent stages calling context as a function pointer remain an unimplemented gap.

@fogydes

fogydes commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

should-fix

Findings

  1. [verified] Clean merge with current main: git merge-tree --write-tree origin/main d8be19d1923fe7890f0fb3089235b0a9ef183dec succeeds cleanly with exit code 0 (merge tree 63c7b695252be52a7edb2c9304daa37ce4ed7f71). Branch is only 420 commits behind origin/main.
  2. [verified] Conventions: python tools/check_conventions.py --base 1189ec3003672bb364dd9f3119ead7b3976e3c83 executed in an isolated scratch worktree and passed cleanly with 0 warnings or errors.
  3. [read-only] PR template checklist: The checklist in the description is missing the mandatory Fixes: <!-- issue it closes as Fixes #N, or none --> line from .github/pull_request_template.md. While the PR body notes Closes #5621., the template requires the explicit checklist entry - [x] Fixes: #5621.
  4. [author-reported] Test claim: Author reports passing Linux guest_module_args test fixture (0.27 s) and mutation controls; test execution not independently run in this review session.

Freshness Envelope

  • Head SHA: d8be19d1923fe7890f0fb3089235b0a9ef183dec
  • Base SHA: 1189ec3003672bb364dd9f3119ead7b3976e3c83
  • Commits behind origin/main: 420
  • git merge-tree with origin/main: exit 0 (clean tree 63c7b695252be52a7edb2c9304daa37ce4ed7f71)

Cross-PR Ledger

  • No other open PRs currently modify core/libs/prx/libkernel/Module/src/Module.cpp or core/libs/prx/libkernel/Module/src/ModuleArgs.cpp.

@MarcZX10

Copy link
Copy Markdown
Contributor

Re-checked — checklist entry still missing

At head d8be19d1923fe7890f0fb3089235b0a9ef183dec, the PR says Closes #5621 in prose but leaves the required Fixes: #N checklist item blank; the repository template specifies that field (template at current main).

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@Nubi-Prey thanks, this resolves the main point of the earlier review: the default block is now only used when no explicit sceKernelLoadStartModule is in progress, explicit loads (including empty ones) pass their own args/argp through, and the pending state is cleared on the exception path too. The tests cover both cases. What remains:

  • Dependencies loaded during an explicit load get the caller's arguments. explicitLoadInProgress is thread-local and stays set for the whole dlopen, so the DT_INIT of every library pulled in as DT_NEEDED by an explicitly loaded module also receives that module's args/argp (null for an empty load) instead of the default block. Please either clear the flag for dependency initialisers, or document that this is intended, with a source.
  • The default block's context is still nullptr. The report on Linux: Cult of the Lamb (PPSA06464) faults at process start in UnifyPS5Plugin.prx, a needed library started with a null arguments block #5621 shows a title calling it as a function at plugin init, so that title now crashes on the null context instead of the null argp. The 0x10/0x200 values and the context semantics are still unmeasured. Please keep this documented as a guess, and drop the test assertion that fails when context isn't null, since it pins an unmeasured value.
  • Debt line: please remove the game name and title ID from the new debt entry. Describe the case without them ("a needed library whose entry function reads argp[0] before checking args"), and link Linux: Cult of the Lamb (PPSA06464) faults at process start in UnifyPS5Plugin.prx, a needed library started with a null arguments block #5621 for the details.
  • Minor: SetPendingModuleArgsForTest_nid_no_patch and GetPendingModuleInitResultForTest_nid_no_patch are now on the production load path in Module.cpp. A name without ForTest would make that clearer.

Reviewed at d8be19d.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 31, 2026
Provide a fallback 16-byte default arguments block {0x10, 0x200, nullptr} in
__aps5_get_pending_module_args_nid_no_patch() when no explicit module load
is in progress.

On Linux, libraries listed under DT_NEEDED execute their DT_INIT at process start
before sceKernelLoadStartModule runs. Unity's UnifyPS5Plugin.prx (Cult of the Lamb,
PPSA06464, boykopovar#5621) reads argp[0] unconditionally expecting {size: 0x10, version: 0x200, context}
or {0x58, 0x103}, which faulted when argp was null.

Limit the default block strictly to initializers running without an in-flight
sceKernelLoadStartModule call, preserving explicit caller arguments including
(0, nullptr) and (N, nullptr).

Move the pending module args state and getter/setter into a dedicated translation
unit (ModuleArgs.cpp) compiled by both libkernel and guest_module_args_host,
ensuring regression tests directly exercise production logic. Extend the test
fixture to verify default argument delivery and explicit null argument preservation.
Document technical debt and known console gaps in docs/dev/debt/libkernel.md.

Closes boykopovar#5621.
@Nubi-Prey
Nubi-Prey force-pushed the fix/libkernel-default-module-args branch from d8be19d to a407a5e Compare October 31, 2026 17:23
@github-actions github-actions Bot removed the unblocks title Names a title ID on the Unblocks line label Oct 31, 2026
@Nubi-Prey

Copy link
Copy Markdown
Author

Rebased onto current main (a407a5ef83 on top of 474030e584) and addressed the review points:

  1. Dependencies loaded during an explicit load (explicitLoadInProgress): Documented in docs/dev/debt/libkernel.md that explicitLoadInProgress stays true across dlopen_nid_postfix, so any non-deferred DT_NEEDED dependency initialized during an explicit load receives that module's args and argp (which can be (0, nullptr)), and that what the console passes to DT_NEEDED dependencies pulled in during sceKernelLoadStartModule is unmeasured.
  2. Default block's context & test assertion: Dropped || defaultArgs->context != nullptr from GuestModuleArgsHost.cpp so the test does not pin an unmeasured value, and kept {0x10, 0x200} documented as a guess in docs/dev/debt/libkernel.md with context unknown (nullptr faults if called during init).
  3. Debt entry in docs/dev/debt/libkernel.md: Removed the game names and title IDs, described the generic case (a needed library whose entry function reads argp[0] before checking args), and linked Linux: Cult of the Lamb (PPSA06464) faults at process start in UnifyPS5Plugin.prx, a needed library started with a null arguments block #5621.
  4. Function naming on the production load path: Renamed SetPendingModuleArgsForTest_nid_no_patch and GetPendingModuleInitResultForTest_nid_no_patch to SetPendingModuleArgs_nid_no_patch and GetPendingModuleInitResult_nid_no_patch across ModuleArgs.hpp, ModuleArgs.cpp, Module.cpp, and GuestModuleArgsHost.cpp. Also updated the PR description checklist to include - [x] Fixes: none.

Architectural note on #5621 / #6586 / #12621: Following the later disassembly and string dumps in #5621 (Plugin args version, found %04x, expected %04x), {0x10, 0x200, context} is the Unity runtime's plugin argument struct passed when the engine calls sceKernelLoadStartModule (which #6586 now defers --module-dir modules to on Linux, and #12621 on Windows, though the Windows path in #5621 was not retested with the title before closure). This branch is ready to merge as-is with the documented fallback, or if you prefer not to pass {0x10, 0x200, nullptr} to non-deferred DT_INIT modules now that #6586 is merged, I can drop the default struct and keep only the ModuleArgs.cpp extraction, exception-safe reset in sceKernelLoadStartModule, and real-production linkage in guest_module_args_host. Let me know which you prefer.

AI-assisted: yes (Gemini 4 Argon)

@github-actions github-actions Bot removed needs rebase Conflicts with main, rebase needed fix error Closes an issue (Fixes #N) labels Oct 31, 2026
@fogydes

fogydes commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Correction: In the previous review (at head d8be19d), the checklist was missing the mandatory Fixes: line. The updated head a407a5e cleanly merges with current origin/main (git merge-tree exits 0, clean tree 2b03619e7d8f5d3dd8d0679c9c051214d668beda), adds - [x] Fixes: none to the checklist, passes check_conventions.py with 0 findings, and maintains a clean single-commit history.

OK

Findings

  1. [verified] Branch freshness & clean merge: git merge-tree --write-tree origin/main a407a5ef836405798b7e248b876acd66aa3f39e7 exits with code 0 (clean tree 2b03619e7d8f5d3dd8d0679c9c051214d668beda). The branch is 1,048 commits behind origin/main and merges without conflict.
  2. [verified] Conventions: python tools/check_conventions.py --base 474030e58424fefdb65e5df9f3820810fbb4553c executed in an isolated scratch worktree and passed cleanly with 0 errors and 0 warnings (All conventions checks passed.).
  3. [verified] Commit hygiene & zero comments: History consists of a single linear Conventional Commit (a407a5ef836). Zero code comments added across all source and test files.
  4. [read-only] PR template checklist: The checklist is fully filled out and compliant, including - [x] Fixes: none, resolving the previous review finding.
  5. [read-only] Technical debt: Unmeasured console semantics and known gaps ({0x10, 0x200} guess and unknown context pointer) are cleanly documented in docs/dev/debt/libkernel.md.
  6. [author-reported] Test claim: Author reports passing Linux x86_64 CTest suite (guest_module_args and guest_deferred_dependencies, 2/2 passed) and verified negative controls; test execution not independently run in this review session.

Freshness Envelope

  • Head SHA: a407a5ef836405798b7e248b876acd66aa3f39e7
  • Base SHA: 474030e58424fefdb65e5df9f3820810fbb4553c
  • Commits behind origin/main: 1,048
  • git merge-tree with origin/main: exit 0 (clean tree 2b03619e7d8f5d3dd8d0679c9c051214d668beda)

Cross-PR Ledger

  • No other open PRs currently modify core/libs/prx/libkernel/Module/src/Module.cpp or core/libs/prx/libkernel/Module/src/ModuleArgs.cpp.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Nov 3, 2026

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

Reviewed — changes requested

This pull request does not merge cleanly into main (4f41f4c): head a407a5e conflicts in

  • core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp

Please rebase it onto current main. I will review the change once it merges cleanly.

@Aryansh1520

Copy link
Copy Markdown
Contributor

We hit the same problem while running Sonic Superstars (PPSA06888) on Linux, and this PR's default argument block fixes it.

What we saw without it: a guest module started through dlopen (SaveData.prx, a Unity plugin) receives a null argp, and its init reads the block before checking the size, so the process faults inside the module's init code. With a zeroed 16-byte argument block in place of the null pointer, the module gets past that point and the title keeps going (save-data files open and the game reaches its in-game level).

The code we tested was a smaller version of the same idea (a zeroed block returned from __aps5_get_pending_module_args_nid_no_patch when argp is null). We are not opening a second PR for it, since this PR covers it and goes further. Your fix is the one to land.

Two things we observed that may matter for this PR:

  • Unity modules are loaded by path from app0/Media/Modules and app0/Media/Plugins. They need to be relinked with --module-dir for those paths, otherwise the load fails with 0x80020002.
  • The explicitLoadInProgress gate means an implicit dlopen gets the default block, which matches what we needed.

If it helps, we can test a rebased version of this PR against the title on our side.

This branch has not been deployed

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

Labels

docs libkernel needs rebase Conflicts with main, rebase needed relinker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants