Skip to content

feat(libc): implement realpath - #1692

Closed
SleglJan wants to merge 1 commit into
boykopovar:mainfrom
SleglJan:feat/libc-realpath
Closed

SleglJan wants to merge 1 commit into
boykopovar:mainfrom
SleglJan:feat/libc-realpath

Conversation

@SleglJan

@SleglJan SleglJan commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds realpath to libc (imported from libSceLibcInternal), in a new src/RealPath.cpp. It follows FreeBSD 11's realpath.c on the guest side:

  • A null path is EINVAL, an empty one ENOENT. A relative path starts from the guest working directory (getcwd); an absolute one from /.
  • Empty and . components are skipped; .. drops the last resolved component and never climbs above /. Every other component must exist, and one that is not a directory with anything after it (file/x, file/, file/., file/..) is ENOTDIR. A missing component is ENOENT even when a later .. would drop it. Backslashes are separators, as everywhere in the path layer.
  • ENAMETOOLONG where FreeBSD gives it: 1024 bytes (PATH_MAX) or more in the input after the leading separator, in the working directory, in the result while a component remains, or in the result after appending one.
  • The result is the guest path as the title spelled it (also through a case-insensitive host match), without a trailing slash. Existence and type come from the host through ResolvePath_nid_no_patch, so a mount alias resolves and a blocked mount is ENOENT; host symlinks are followed but not reflected in the result, since the guest filesystem exposes no symlinks.
  • A null resolved is a PATH_MAX buffer from the application heap, which the title's free releases (libcurl does, lib/vtls/vtls_scache.c). With a caller's buffer, a failure leaves the path that failed in it, as FreeBSD does.

It does not depend on #1624 (lstat) or #1627 (readlink): the resolution is lexical plus host existence checks, which is what the console's filesystem makes observable.

Overlap: #1729 (syslog) adds its source line to the same list in libc/CMakeLists.txt; whichever merges second rebases one line.

In the Linux audit of the homebrew catalog (#1578), realpath was absent for 17 of the 34 relinked titles. The callers are statically linked libraries: Mesa's util_dl_get_path_from_proc, libcurl's CA path keys and fontconfig's FcStrRealPath; all of them accept a null return.

Tested

  • Linux (CachyOS, kernel 7.2.9, GCC 16.2.1), full build with BUILD_TESTING=ON and ANYPS5_ENABLE_SPIRV_TOOLS=ON on this branch (based on main 4b005b2): ctest --repeat until-fail:3 467/467; tools/check_test_registration.py and tools/check_conventions.py --base upstream/main pass.
  • New guest_realpath test on a directory tree under the guest root: /, a directory with and without a trailing slash, .., ., doubled slashes, .. above the root, backslashes, an upper-cased directory name returned as typed, relative paths after chdir including .. back to /, a mount alias and a blocked mount, the allocated form freed with free; EINVAL for null, ENOENT for empty and for a missing component before .., ENOTDIR for the four non-directory forms, ENAMETOOLONG for an input of 1024 bytes and for / plus 1023 bytes (the append boundary), ENOENT for a 1023-byte path of missing components, and the failing path left in the caller's buffer in each case. Written before the implementation; it failed on the unresolved export and passes with it.
  • The patched libc.prx exports the NID (vhtcIgZG-Lk).
  • tools/import_audit.py on five of the 17 catalog titles, absent imports before → after: PPSA59322 ccleste 50 → 49, PPSA99010 vkQuake 51 → 50, PPSA99505 Jelly5 30 → 29, PPSA99082 OpenNOW 36 → 35, PPSA99808 PICO-8 28 → 27. The before column is the Four open-source homebrew titles stop at start-up on unresolved imports (sysctl, pthread_set_name_np, isatty, mkstemp, signal sets, Opus decoders) #1578 audit (main 4b005b2 + fix(relinker): keep Linux imports declared from host modules off guest exports #1529, which changes binding, not which NIDs the built libraries export); the after column is this branch on main 4b005b2. None of them loads yet; the remaining imports are listed in Four open-source homebrew titles stop at start-up on unresolved imports (sysctl, pthread_set_name_np, isatty, mkstemp, signal sets, Opus decoders) #1578.
  • Windows: the same Build workflow run on this commit in the fork's Actions (merged onto main 6b56175), run 37822095572: guest_realpath passes its three repeats, full suite 474/474. hikazey ran this commit merged onto main b858fdc on Windows 11 with MinGW-w64 GCC 15.2.0: guest_realpath 5/5, the full suite 478/478, and the test fails 5/5 with the moreFollows && !is_directory check disabled, so the ENOTDIR, backslash, case-insensitive and ENAMETOOLONG cases hold against Win32's path normalisation (comment). The code is platform-neutral (std::filesystem over the resolver's native paths) and the test is registered on both platforms like guest_working_directory.

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)

🤖 Generated with Claude Code

Resolves a guest path the way FreeBSD 11's realpath does: a relative
path starts from the guest working directory, "." and empty components
are skipped, ".." drops the last component and never climbs above "/",
every other component must exist, and one that is not a directory with
anything after it is ENOTDIR. Backslashes separate components as in the
rest of the path layer. The result is the guest path; the host is
consulted only for existence and type, so host symlinks are followed
but not reflected. A null buffer is PATH_MAX bytes from the application
heap, a null path is EINVAL, an empty one ENOENT, and 1024 bytes or more
in the input, the working directory or the result is ENAMETOOLONG. On
failure the caller's buffer holds the path that failed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hikazey

hikazey commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Windows run, since the description lists Linux only: Windows 11 Pro (26200), MinGW-w64 GCC 15.2.0 (WinLibs r7, the CI build), Release, configured like CI, Ryzen 9 5900X. This commit merged onto main b858fdc4, which merges cleanly.

build guest_realpath
this PR 5/5 passed
the moreFollows && !is_directory check disabled, test kept 5/5 failed
full suite with this PR 478/478 passed

I checked this one on Windows because Win32 path normalization drops trailing . and separators, so file/. or file/ could look like file to the host. The component-by-component walk avoids that: the ENOTDIR cases hold on Windows, as do the backslash, case-insensitive spelling and ENAMETOOLONG cases in the test.

AI-assisted: yes (Claude Code).

@SleglJan

SleglJan commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the Windows run and the normalisation angle; the mutation check is the useful part. Added to the Tested section.

@github-actions

github-actions Bot commented Oct 8, 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
#1729 core/libs/prx/libc/CMakeLists.txt L35

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: realpath isn't a title's stop on main. In the PICO-8 (PPSA99808) run in #1578 it comes after nine other missing imports, one of them without a PR yet. Reopen when it's a title's next stop on main, with the log line.

@Amoenus

Amoenus commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Additional Windows validation of this proposal, without requesting reopening under the current first-stop rule.

Tested PR head 57f8d2390a1d968151fe71a0311c9a94445581d7 applied locally to main 57f96fd2b36563e8380c603a92b1ef0d9d3a6aa4, retaining main's libc source list. Windows 11 Enterprise 26200, WinLibs r7 MinGW-w64 GCC 15.2.0, Release, BUILD_TESTING=ON. Full system-library build passed; guest_realpath and guest_working_directory passed 5/5 runs each. This is not a full-suite or Linux result.

An additional host-symlink probe failed 5/5 runs. Under the guest filesystem root, create:

fixture/
  marker.txt
  sub/marker.txt
  sub/target.txt
  sub/deep/
  link.txt -> sub/target.txt
  linkdir -> sub/deep

Create the file and directory links with CreateSymbolicLinkW (Developer Mode enabled). Calling the proposal's realpath_nid_postfix with a 1024-byte output buffer gives:

Input Expected after link resolution Observed
/fixture/link.txt /fixture/sub/target.txt /fixture/link.txt
/fixture/linkdir/../marker.txt /fixture/sub/marker.txt /fixture/marker.txt

Both marker files exist, so the second result is successful resolution through the wrong parent, rather than just a missing-file error. The proposal already mentions that host links are followed without reflecting their targets in the returned path; the second case demonstrates an additional behavioral consequence of that limitation.

There is also a toolchain trap when guarding this case. On these native links, a standalone host-only probe reported:

file:      symlink_status type=1, is_symlink=0, error=0; GetFileAttributesW=0x420
directory: symlink_status type=2, is_symlink=0, error=0; GetFileAttributesW=0x410

The native attributes include FILE_ATTRIBUTE_REPARSE_POINT, but this MinGW standard-library path reports the target's ordinary file/directory type. std::filesystem::create_symlink also returned ENOSYS here; native fixture creation worked.

A local diagnostic guard using Windows reparse-point attributes, and symlink_status on other hosts, throws explicitly for these unsupported components rather than returning an unresolved or incorrectly normalized path. The expanded test checks both file links and directory-link/.., the exact exception, and that the output buffer is untouched. Full system libraries built; both tests passed 5/5. Removing the guard caused the new realpath test to fail, then restoring it returned both tests to 5/5. Conventions and test-registration checks passed. This guard rejects every Windows reparse point, does not implement guest link-target translation, requires link creation privileges or Developer Mode for its Windows fixture, and has not been tested on Linux.

In our PPSA99902 investigation, realpath appears only after a local stack of other import fixes/placeholders. It is not the title's next stop on unchanged main. No guest entry or gameplay was reached, and no realpath execution by the title has been observed. No new PR was created.

AI-assisted investigation and local changes: Codex. All claims above are from local builds, behavioral probes, and controlled startup logs; console behavior remains unverified.

Fixture follow-up: the first diagnostic fixture used forward slashes in Windows reparse targets, which affected native lookup. I repeated the probe with native relative targets sub\target.txt and sub\deep, verified that reading the file link returns the target contents and that resolving the directory link reaches sub/deep, and obtained the same two realpath failures in all five runs. Expected .. behavior is the guest/FreeBSD behavior: FreeBSD 11 realpath.c expands a link target back into the remaining component walk before processing subsequent components. Windows native lexical normalization of linkdir/.. is not used as the oracle.

@babyhuey

Copy link
Copy Markdown
Contributor

@SleglJan with gethostname (#2968) merged, XashPS5 (PPSA19111) now stops at realpath on main f38a036 (catalog build, sce_module/libc.prx unwrapped): app.elf: symbol lookup error: app.elf: undefined symbol: vhtcIgZG-Lk. Ruffle (PPSA68091) also reaches it after a few open fixes. That's the condition this was closed on, and the intake check is at 10 merged PRs, so it can go up again as a new PR.

@SleglJan SleglJan mentioned this pull request Oct 10, 2026
9 tasks done
@SleglJan

Copy link
Copy Markdown
Contributor Author

@babyhuey thanks, reopened as #3147, rebased onto main e4045bb. On that main, XashPS5 stops at vhtcIgZG-Lk as you reported. With the branch, its loader stops at the next import, cjt-4ySmYTs (libkernel_sys). The description also covers @Amoenus's symlink probe: .. stays lexical, as for open, and the difference from FreeBSD is listed in TechnicalDebt.

AI-assisted: yes (Claude Code)

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.

5 participants