Skip to content

feat(libc): implement realpath - #3147

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
SleglJan:feat/libc-realpath
Oct 10, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
SleglJan:feat/libc-realpath

Conversation

@SleglJan

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 result while a component remains, or in the result after appending one. A working directory that long already fails in getcwd.
  • 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; a host symlink is followed for existence and type but not expanded (see below).
  • 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.

Symlinks: the console's guest filesystem has none, but a host directory under the guest root can. This realpath does not expand a host link, and it drops .. lexically. That is how the path layer resolves every guest path for open and stat (Resolve in libc/src/General.cpp uses lexically_normal), so the result names the same file the title opens with the input. @Amoenus's probe on #1692 shows the difference from FreeBSD: through linkdir -> sub/deep, /fixture/linkdir/../marker.txt gives /fixture/marker.txt, where FreeBSD gives /fixture/sub/marker.txt. It is listed in TechnicalDebt. #1624 (lstat) and #1627 (readlink), merged since #1692, report host links to the guest on Linux. Expanding them here would make realpath of linkdir/../x name a different file than open of it.

This is XashPS5's stop on main now that #2968 (gethostname) is merged, as @babyhuey reported on #1692. #1692 was closed because realpath was not a title's stop then; this is the same change rebased onto current main.

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), Ryzen 9 7950X3D, RTX 3080 (NVIDIA 615.71.09), Release with BUILD_TESTING=ON and ANYPS5_ENABLE_SPIRV_TOOLS=ON, this branch on main e4045bb:
  • 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 longer than 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 the missing-component cases, for file/ and file/x, and at the append boundary. Written before the implementation; it failed on the unresolved export and passes with it.
  • Windows, from feat(libc): implement realpath #1692 (identical RealPath.cpp and test, older main):
    • the fork's Build workflow, run 37822095572, with guest_realpath passing 3 repeats and the full suite 474/474;
    • @hikazey's run: guest_realpath 5/5, and 5/5 failing with the ENOTDIR check disabled;
    • @Amoenus's run: guest_realpath and guest_working_directory 5/5 each.
  • XashPS5 (PPSA19111, catalog build, relinked with its sce_module/libc.prx unwrapped) in a bubblewrap sandbox:
    • On main e4045bb (Release, tests and SPIRV-Tools off) the loader stops with symbol lookup error: app.elf: undefined symbol: vhtcIgZG-Lk (exit 127). The import audit lists 18 absent imports.
    • With this branch, 17 are absent, and the loader stops at the next one, cjt-4ySmYTs (libkernel_sys).

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

https://claude.ai/code/session_01Fnrk5Dazjb2tLooVSpdRQq

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 expanded, and ".." is lexical, as in the rest of the path layer. 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 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fnrk5Dazjb2tLooVSpdRQq
@SleglJan SleglJan mentioned this pull request Oct 10, 2026
9 tasks done
@github-actions

Copy link
Copy Markdown

Progress report (5b4709f → e1f168a)

➖ System libraries: 85.53% (+0.0%, +1 implemented functions)

✅ 1 implemented
Library Name
libc realpath_nid_postfix

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on Linux (merged onto current main): build and guest_realpath pass, and the test no longer links without RealPath.cpp. Error codes (EINVAL, ENOENT, ENOTDIR, ENAMETOOLONG boundaries, failing path left in the caller's buffer) match FreeBSD 11 realpath.c, the lexical .. deviation is listed in TechnicalDebt, and the Windows runs in the thread cover the same code. Merging once CI is green.

@adriadam10
adriadam10 merged commit 1e52f94 into boykopovar:main Oct 10, 2026
15 checks passed
@babyhuey

Copy link
Copy Markdown
Contributor

Windows title run to go with the Linux one above: e1f168a merged onto main 2b5d119 (clean), Windows 11, MinGW-w64 GCC 15.2 from WinLibs, Release, XashPS5 (PPSA19111) catalog build with sce_module/libc.prx unwrapped.

  • guest_realpath passes 5/5 (--repeat until-fail:5), no warnings.
  • main: FAIL: unresolved ELF import vhtcIgZG-Lk (realpath) from libSceLibcInternal.prx
  • with this PR: FAIL: unresolved ELF import cjt-4ySmYTs (fork) from libkernel_sys.prx, the same next stop as on Linux (also checked here, undefined symbol: cjt-4ySmYTs).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants