Skip to content

feat(libkernel): export the POSIX rename - #1192

Merged
adriadam10 merged 4 commits into
boykopovar:mainfrom
ps54all:feat/libkernel-posix-rename
Oct 9, 2026
Merged

adriadam10 merged 4 commits into
boykopovar:mainfrom
ps54all:feat/libkernel-posix-rename

Conversation

@ps54all

@ps54all ps54all commented Oct 7, 2026 •

Copy link
Copy Markdown

What

PPSA12544 (Hollow Knight: Silksong, v1.0.30021) imports rename (NID NN01qLRhiqU) from libkernel, not from libc. The title ships its own libc.prx in sce_module, so the host libc's rename (added in 91bf8e3) is not in the lookup. libkernel did not export it, and with --windows-diagnostics startup stopped with:

FAIL: unresolved ELF import NN01qLRhiqU from libkernel.prx
  • libkernel now exports rename next to unlink. Like the other POSIX wrappers in Stdio.cpp, it checks both paths with PathError (null ? EFAULT, empty ? ENOENT) and maps the sceKernelRename result to errno through PosixResult.
  • Test: guest_filesystem_tests links both libkernel and libc, which each define rename_nid_postfix, so which one it binds to depends on the loader (reviewer on Linux showed it passes with or without this change). The new guest_kernel_rename_tests resolves the symbol from libkernel.prx explicitly and checks rename, rename onto itself, missing source (ENOENT) and null/empty arguments (EFAULT/ENOENT). It fails without the export. Windows only so far; the dlopen path for Linux is untested.
  • Searched all open PRs: none adds rename to libkernel.

Tested

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
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes (Claude Code). I reviewed and take responsibility for every line.

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

Built and ran it — and then found that the existing test for this symbol cannot tell whether your change is there, which is worth knowing before this is relied on as covered.

Linux (CachyOS, GCC, Release):

cmake --build build --target guest_filesystem_tests --parallel 16
ctest -R ^guest_filesystem$    →  1/1 Passed

The suite passes with and without the change. guest_filesystem_tests links both libkernel and libc, and libc.prx already exports this symbol on your base — libc/src/Filesystem.cpp:93:

extern "C" int APS5_VABI rename_nid_postfix(const char* from, const char* to) {
    if (!from || !to) { errno = 14; return -1; }
    ...
    std::filesystem::rename(source, destination, error);

nm on the test binary shows the symbol as U rename_nid_postfix — undefined in the executable and bound at load time from one of the two libraries (ldd lists both libkernel.prx and libc.prx). Reverting only your Stdio.cpp hunk and rebuilding:

pr-1192   (libkernel defines it):  100% passed
pr-1192^  (only libc defines it):  100% passed

So the test binds to whichever library the loader picks and passes either way; it exercises a rename, not the one you added. The existing assertions (rename to a new name, to itself, to a missing source, back again) are real coverage of the function — just not of this implementation.

The thing I would ask about is the second implementation. There are now two definitions of one POSIX function, and they do not follow the same path:

  • libc: rejects a null argument as EFAULT (14) and an empty one as ENOENT (2), then ResolvePath + std::filesystem::rename with FilesystemError mapping.
  • libkernel (yours): PathError on both arguments, then sceKernelRename through PosixResult.

Both are defensible, and each is internal-consistent — but a guest that imports rename from libkernel and one that imports it from libc now run different validation and different errno mapping, and two implementations of one function drift as soon as either is edited. If a title or a NID needs rename exported from libkernel specifically, putting that in the body makes the duplication obviously intentional; if not, one of the two delegating to the other would keep a single behaviour.

Nothing blocking — the build and the suite are clean, and adding the export is not harmful. It is the claim of coverage I wanted to correct, because the passing test looks like it covers this and does not.

This is a comment, not an approval — I have no merge rights here.

Written with AI assistance.

@ps54all

ps54all commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thanks for the careful check, and you're right: guest_filesystem_tests links both libs, so it binds to whichever the loader picks. My claim that it ran against libkernel only held on Windows. I've corrected the description and pushed 72dca19, which adds guest_kernel_rename_tests. It resolves rename_nid_postfix from libkernel.prx explicitly. It fails without the export and passes with it (Windows; I couldn't test the Linux dlopen path).

On the duplicate: the export has to exist in libkernel because Silksong ships its own libc.prx, so host libc is never in its lookup. I agree two implementations will drift. I'd rather make libc delegate to the libkernel one in a separate PR than widen this one, unless you'd prefer it here.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Built and ran this merged onto current main on Linux: everything passes except the new test.

  • core/libs/tests/GuestKernelRename.cpp:36: guest_kernel_rename aborts on Linux at the first rename (Kernel rename check failed at line 42 (guest errno 2)). root is built from std::filesystem::temp_directory_path(), an absolute host path, and the guest VFS on Linux doesn't map those. GuestFilesystem.cpp:89 uses a relative directory (std::filesystem::path("anyps5-filesystem-test-" + ...)) for this reason; ctest runs in the build dir. With root built the same way the test passes here. Please change it, so the Linux CI job doesn't go red.

The export itself looks fine: it matches the other POSIX wrappers in Stdio.cpp, and since the title imports rename from libkernel rather than libc, having it there is intentional, not a duplicate to remove.

Not verified on our side: Windows (the LoadLibraryA path and the Silksong import).

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed build and removed needs rebase Conflicts with main, rebase needed labels Oct 8, 2026
@ps54all

ps54all commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thanks @adriadam10, good catch. Fixed in 5b84912: the test now builds its directory relative to the working directory, the same way GuestFilesystem.cpp does, instead of using temp_directory_path(). It passes on Windows; I can't run Linux, so the Linux CI job is the check there.

I also merged current main (e4868c3) to clear the conflict in libkernel/CMakeLists.txt, where guest_kernel_sync was added next to our block. Both are kept.

On Windows, the LoadLibraryA path is what guest_kernel_rename exercises and it passes there. Thanks for confirming the export itself is fine as is, so I'll drop the idea of making libc delegate to it.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked on Linux, merged onto current main: build, libs and full ctest green (434/434), and the relative-directory fix in 5b84912 works. Two things before merging:

  • The conventions check fails: [duplicate-export] rename_nid_postfix is also in libc (Stdio.cpp:648). The project rule (#228, #241) is one implementation per export. Since the title ships its own libc.prx and only loads libkernel's rename, this is the case the shared-source pattern is for (FontLibrary.cmake): keep a single rename definition in one source compiled into both libraries, instead of a second copy in Stdio.cpp.
  • On Linux the new test also passes with the export removed: dlsym on the libkernel handle also searches its dependencies, and libc exports the symbol. Check with dladdr that the symbol comes from libkernel (we have a version of this ready: it fails without the export and passes with it), so the test pins the export.

Not verified on our side: Windows (LoadLibraryA path, the Silksong import).

@ps54all
ps54all force-pushed the feat/libkernel-posix-rename branch from 5b84912 to 76db9fa Compare October 8, 2026 14:14
@ps54all

ps54all commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thanks @adriadam10, both items are done in 76db9fa, rebased onto current main.

  • One implementation: libc's rename now lives in libc/src/Rename.cpp, compiled into both libc and libkernel. The copy in libkernel's Stdio.cpp is gone. FilesystemError moved to libc/include/FilesystemError.hpp so the shared source can use it. tools/check_conventions.py passes locally.
  • Test pins the export: guest_kernel_rename now checks the symbol it resolves belongs to libkernel, using dladdr against sceKernelRename from the same handle on Linux and the owning module handle on Windows. On Windows it fails with Rename.cpp removed from libkernel and passes with it.

On Windows, guest_kernel_rename and guest_filesystem pass. In the full ctest run, 3 of 457 failed: jmprel_table_bounds, windows_icon_resource and strict_nid_filter. They need the full build, which fails here on a freetype resource-compile error (ftver.rc), so I don't think they are related to this change, but I didn't run them on main. Linux is untested on my side, so your run is the check for the dladdr path.

@Dextroid17

Copy link
Copy Markdown
Contributor

Verified the current head on Linux, and both points @adriadam10 left hold here.

Linux (CachyOS, GCC, Release), head 76db9fa0:

cmake --build build --target guest_kernel_rename_tests --parallel 16
./tests/guest_kernel_rename_tests                        → rc=0   (three consecutive runs)
ctest -R ^guest_kernel_rename$                           → 1/1 Passed
ctest -j 16                                              → 466/466 Passed   (26.14 s)
python3 tools/check_conventions.py --base upstream/main  → All conventions checks passed.

The test now pins the libkernel export, which is the thing I could not get out of the old one. With the single line that compiles Rename.cpp into libkernel removed (libkernel/CMakeLists.txt), while libc.prx still exports the symbol:

nm -D libkernel.prx | grep rename_nid_postfix  →  (no output)
./tests/guest_kernel_rename_tests              →  rename_nid_postfix is not defined by libkernel   rc=1

Restored, it passes again and nm -D libkernel.prx shows T rename_nid_postfix. So the dladdr comparison rejects the libc copy on Linux too, not only on Windows — that is the half you noted as untested, and it does the job here.

One implementation: nm -D still shows rename_nid_postfix in both libkernel.prx and libc.prx, but both come from the single libc/src/Rename.cpp, and the conventions check that failed on the previous head ([duplicate-export] rename_nid_postfix is also in libc) now passes. That is the shared-source shape @adriadam10 pointed at.

Nothing else from me on the test: the coverage claim I raised is now backed by a control rather than by a passing run. Not verified here: Windows (the LoadLibraryA/Silksong path) — your re-check on Linux is the CI-side one.

Written with AI assistance.

@Dextroid17

Copy link
Copy Markdown
Contributor

Status on the current head, since both of your points are now done and the remaining blocker is not in the code.

Linux, head 76db9fa0 (posted above in full): guest_kernel_rename rc=0 across three runs, ctest -j16 466/466, conventions check passes, and the dladdr pin is backed by a control — with the one line that compiles Rename.cpp into libkernel removed, the test fails with rename_nid_postfix is not defined by libkernel and nm -D libkernel.prx no longer lists the symbol. So the half you flagged as untested on Linux works there; the single-implementation shape resolves the [duplicate-export] failure.

The thing actually holding the merge: no CI has run on this head. The workflow run for 76db9fa0 is in action_required — GitHub is waiting for a maintainer with write access to approve it before it starts:

  gh run list --repo boykopovar/AnyPS5 --branch feat/libkernel-posix-rename
  2026-10-08T14:14:36Z [pull_request] completed/action_required  76db9fa0
  gh run view 37790853088        →  "Triggered via pull_request", JOBS: (empty)

So no checks reported on the 'feat/libkernel-posix-rename' branch means waiting for approval, not ran and reported nothing. It is not specific to this PR either: 18 of the last 100 runs in this repository are sitting in the same state, and two others I looked at this morning are identical — #1446 (head fb2ecdd4) and #1512 (head 8a857904) both show no checks for the same reason.

@adriadam10 — if you approve the run for this head (and the other two), CI becomes the check rather than a missing signal, and the Windows LoadLibraryA/Silksong path is the only thing left that no one here has exercised.

Written with AI assistance.

@Dextroid17

Copy link
Copy Markdown
Contributor

The full list is now in one place: #1631 — 38 open PR heads queued at action_required, oldest first, with a command to refresh it. Addressed to the approval side rather than per-PR.

Written with AI assistance.

ps54all and others added 4 commits October 8, 2026 23:17
Titles that ship their own libc in sce_module import rename from
libkernel (NID NN01qLRhiqU), where it was missing, so PPSA12544 failed
to start. It checks both paths like unlink and maps sceKernelRename's
result to errno.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
guest_filesystem_tests links libkernel and libc, both of which define
rename_nid_postfix, so which one it binds to depends on the loader and the
suite passes either way. Resolve the symbol from libkernel.prx explicitly so
the export added for titles that ship their own libc.prx is actually tested.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The guest VFS on Linux does not map absolute host paths, so the test failed
at the first rename there. Match GuestFilesystem.cpp and build the directory
relative to the working directory.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The repo allows one implementation per export. Move libc's rename into
libc/src/Rename.cpp, compile that file into both libc and libkernel, and
drop the second copy from libkernel's Stdio.cpp. FilesystemError moves to a
header so the shared source can use it.

The test now checks that the symbol it resolves belongs to libkernel (module
handle on Windows, dladdr on Linux) so it fails when the export is removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ps54all
ps54all force-pushed the feat/libkernel-posix-rename branch from 76db9fa to 1bd0ef8 Compare October 8, 2026 21:20
@ps54all

ps54all commented Oct 8, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (head 1bd0ef8c) to pick up 59fd202. The Linux failure on the previous head was guest_formatting_tests is run by ctest now; remove it from KNOWN_UNRUN, a stale entry in tools/check_test_registration.py that was already fixed on main, so it was not caused by this PR. check_conventions.py passes locally. The new head will need a workflow approval before CI runs.

@Dextroid17

Copy link
Copy Markdown
Contributor

Rebase noted. The question from my review is still the one that decides this, and it is the same one the maintainer puts to other system-library changes: which title imports rename from libkernel?

On main today the symbol exists in exactly one place — libc/src/Filesystem.cpp — and this adds a second implementation in a second library. They do not follow the same path: libc rejects a null argument as EFAULT and an empty one as ENOENT, while libkernel routes both through PathError. With a title ID and the log line the duplication is justified and this can land; without one, two implementations of one POSIX function will drift, and which a title gets is decided by the loader's search order rather than by intent.

The rebase itself looks clean from here.

Written with AI assistance.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked on Linux merged onto current main: build, libs and ctest green apart from patched_libraries, which depends on directory iteration order (libs without RUNPATH needing libkernel.prx) and also fails on unrelated branches. guest_kernel_rename passes, and fails with "rename_nid_postfix is not defined by libkernel" when Rename.cpp is dropped from libkernel's sources. libc and libkernel now share one rename (libc/src/Rename.cpp), so the earlier concern about two implementations drifting no longer applies, and the PR body names the title (PPSA12544) and the unresolved-import log line. Merging once CI is green.

@adriadam10
adriadam10 merged commit 09a3462 into boykopovar:main Oct 9, 2026
6 checks passed
@ps54all

ps54all commented Oct 10, 2026

Copy link
Copy Markdown
Author

To close the loop on the question: PPSA12544 (Hollow Knight: Silksong) imports rename from libkernel. It ships its own libc.prx, so the host libc export is never in its lookup. The merged version keeps one implementation (libc/src/Rename.cpp, built into both libraries), so the two cannot drift.

Written with AI assistance.

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.

3 participants