Skip to content

feat(libc): read guest va_lists portably in lifted builds - #11

Open
oberonlift wants to merge 4 commits into
lifted-libc-exceptionsfrom
lifted-libc-va-list
Open

oberonlift wants to merge 4 commits into
lifted-libc-exceptionsfrom
lifted-libc-va-list

Conversation

@oberonlift

@oberonlift oberonlift commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Open on AnyPS5 as boykopovar#15801; this copy follows it.

Merge after #10. Until the PRs before it merge, this PR also shows their changes; #11 shows only its own.

ps5lift is a static recompiler for PS5 executables: it lifts a game's x86-64 code to LLVM IR and compiles it ahead of time to aarch64, which runs on Linux against AnyPS5's own libraries with no interpreter or JIT. It runs AnyPS5's PS5-style test programs and a homebrew media player to its home screen; commercial games are not playable yet. Branch ps5lift-preview merges all of its PRs (how to try it); native Apple Silicon support is coming along fast. It is a separate approach from the ARM64 compiler in boykopovar#5021; boykopovar#1835 has an introduction.

This is 5 of 8 PRs that let AnyPS5's libraries run a lifted program; the lifter itself follows them.

x86-64: behaviour unchanged. The shared APS5_VA_BEGIN macro expands to the code it replaces, Audio3d's delivery loop moved into a helper, and the host-list handling in FormatArguments and the AudioPropagation change are compiled only in lifted builds.

What

  • In a lifted build every VaList is a SysV va_list built by the guest or by a bridge, so libc formats and scans through FormatArguments and its in-process scanner instead of the host's vprintf and vsscanf.
  • libc's own variadic functions wrap the host va_list in a tagged VaList through the shared APS5_VA_BEGIN macro, which libSceLibcInternal's err and warn and the formatting and scanning tests now use as well.
  • libSceAudio3d hands a report handler, which is guest code, a SysV va_list built around the call's values. It copied its own host va_list, which has that layout only on an x86-64 host.
  • In lifted builds libSceAudioPropagation's sceAudioPropagationReportApi, which copies its own host va_list into the report, stops as not implemented when host code calls it; guest calls stop at the bridge. The debt file lists it, and the library's test checks the rejection in lifted builds in place of its report checks, which read the report's va_list as the host's.
  • In lifted builds vasprintf formats through FormatWindows, as Windows builds do, so it reads only the guest's 24-byte VaList; and printf_s orients stdout for bytes, as the other builds' printf_s does.

boykopovar#5021 also changes some of the same files: Formatting.cpp, WindowsFormatting.hpp.

Tested

Compiled for x86-64 Linux before and after this PR (GCC 13, same flags): 16 of the 19 objects it reaches on both sides are byte-identical; the other three are libSceAudio3d's Export.cpp, whose delivery loop moved into a helper, and the two libc tests that gained checks. AudioPropagation's Export.cpp and test are byte-identical.

On a DGX Spark (aarch64 Linux, NVIDIA GB10, LLVM 18), with all eight PRs and ps5lift on top: every library builds for a lifted program, 2,288 of AnyPS5's own tests pass in that build on lavapipe, and ps5lift's 69 tests pass, among them its libc, exceptions, kernel, fiber and module-loading fixtures and upstream's libc test programs.

The conventions check passes on this branch.

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: feat(libc): unwind guest frames and keep the host's exception runtime in lifted builds #10
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 5ee5806 to 7c87f5a Compare November 1, 2026 19:35
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 7c87f5a to d91f0c5 Compare November 1, 2026 22:07
@oberonlift
oberonlift force-pushed the lifted-libc-va-list branch 2 times, most recently from 18527d4 to c8cd394 Compare November 2, 2026 17:02
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch 2 times, most recently from 7ba4609 to f98143b Compare November 2, 2026 18:30
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from f98143b to 60cb2c7 Compare November 2, 2026 22:07
@oberonlift
oberonlift force-pushed the lifted-libc-va-list branch 2 times, most recently from 24d0f1d to ca2b1c4 Compare November 3, 2026 16:34
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 60cb2c7 to 6a4e3b7 Compare November 3, 2026 16:34
@oberonlift

Copy link
Copy Markdown
Owner Author

Review findings (unverified)

Code review of ca2b1c47e6. None of these have been built, run or checked by a second reviewer yet. Most severe first.

  1. core/libs/prx/libc/include/WindowsFormatting.hpp:45: some new code is not behind ANYPS5_GUEST_LIFTED. The HostVaListTag check, the va_copy/va_end in FormatArguments and the extended-%Lf scan path in ScanSource compile into x86 builds too, though only lifted builds create tagged lists. The review suggests four x86 objects differ from the base. That would contradict the x86-64: line, so redo the comparison for these files. On x86, %Lf scans would also copy all 16 bytes of a local long double into the guest target where host scanf wrote 10, so 6 indeterminate padding bytes reach guest memory.
  2. core/libs/prx/libc/src/Formatting.cpp:516: vasprintf reads past a VaList. In lifted builds vasprintf_nid_postfix still runs va_copy(copy, *reinterpret_cast<std::va_list*>(args)) before choosing a path. On aarch64 that reads a 32-byte AAPCS64 va_list out of a 24-byte VaList, 8 bytes past asprintf's/dprintf's stack VaList args[1] or past a guest object, and the copy is never used because RequiresGuestFormatting is always true there.
  3. core/libs/prx/libc/src/Formatting.cpp:929: the lifted printf_s_nid_postfix skips _Stdout_nid_postfix.Orientation(-1). The Windows and x86 versions and vprintf_nid_postfix call it, and PrintWindows does not. After printf_s, fwide(stdout, 0) returns 0 instead of -1, so a later wide write succeeds where it should fail.
  4. core/libs/prx/libSceNKWebKit/src/WtfAssert.cpp:346: WTF::dataLogFV still passes the guest's SysV va_list to host std::vfprintf. The aarch64 va_list has a different layout, so it prints garbage or crashes on %s.
  5. core/libs/prx/libc/src/Formatting.cpp:451: long double handling for host-tagged lists is inconsistent. FetchHost<long double> reads a host binary128 value, but the new %Lf scan path always writes guest x87 bytes through GuestLongDoubleStore, even when the pointer comes from a host caller. ScanningWide.cpp:372 does the same. Host sscanf_nid_postfix("2.25", "%Lf", &hostLd) stores x87 bytes into a binary128 variable.
  6. core/libs/prx/libc/src/Formatting.cpp:447: scanned %Lf/%Le/%Lg truncate instead of rounding. They parse into binary128 and GuestLongDoubleStore drops the extra significand bits. sscanf("0.1", "%Lf", &x) gives 0xCCCCCCCCCCCCCCCC where x87 strtold gives 0xCCCCCCCCCCCCCCCD.
  7. core/libs/prx/libc/src/Formatting.cpp:98: lifted scans lose positional %n$. ScansInProcess sends every lifted scan through ScanSource, which reads %1$d's digits as a width and stops at $. x86 builds hand non-wide formats to glibc, which supports them. This only matters if the PS5 libc accepts positional scans.
  8. core/libs/prx/libc/src/Formatting.cpp:100: the lifted ScansInProcess calls ScansWideInProcess(format) and discards the result. It is called only for the side effect of RequireNoPositionalWideScan. Writing it as ScanGuestStream does (if (HasGuestWideScan(format)) LibcDetail::RequireNoPositionalWideScan(format); return true;) keeps it from looking dead.
  9. core/libs/prx/libc/src/Formatting.cpp:39: host fallback branches are unreachable in lifted builds. With RequiresGuestFormatting and ScansInProcess constant true, the host paths in vfprintf, vprintf, vsprintf, vsnprintf, vsprintf_s, vsscanf and vfscanf (va_copy plus the host call) are compiled but never run. They are where the vasprintf over-read above comes from.
  10. core/libs/prx/libSceAudio3d/Export.cpp:266: the SysV va_list layout is now spelled out in three places. Audio3d builds one by hand with its own 48 and 176, FormatArguments::Fetch hard-codes the same constants, and so does SceTypes. GuestVaList.hpp is the natural home for the constants and a helper that builds a list over an array of values.

@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 6a4e3b7 to f0a368b Compare November 4, 2026 15:40
@oberonlift
oberonlift force-pushed the lifted-libc-va-list branch 2 times, most recently from 9ec5a9d to 63dc196 Compare November 4, 2026 16:20
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from f0a368b to 23da0d9 Compare November 4, 2026 16:20
@oberonlift
oberonlift force-pushed the lifted-libc-va-list branch 2 times, most recently from 48afcc4 to 9cc882f Compare November 4, 2026 20:18
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch 2 times, most recently from ba34115 to 56040c8 Compare November 5, 2026 01:21
@oberonlift
oberonlift force-pushed the lifted-libc-va-list branch 4 times, most recently from 915b304 to a0fa69d Compare November 6, 2026 00:18
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 1da9fef to 2fa99de Compare November 6, 2026 00:18
@oberonlift
oberonlift force-pushed the lifted-libc-exceptions branch from 2fa99de to 98e0f9a Compare November 6, 2026 15:35
oberonlift and others added 4 commits November 6, 2026 15:54
In a lifted build every VaList is a SysV va_list built by the guest or
by a bridge, so the libc formats and scans through FormatArguments and
its in-process scanner instead of the host's vprintf and vsscanf. The
libc's own variadic functions wrap the host va_list in a tagged VaList
through the shared APS5_VA_BEGIN macro, which the formatting and
scanning tests now use as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A report handler is guest code, which reads the call's arguments through
the x86-64 SysV va_list in the report. The dispatcher copied its own
host va_list there, which is that layout only on an x86-64 host, so a
lifted build for aarch64 did not compile. In lifted builds the report
now carries a SysV va_list built around the call's values: the named
parameters fill every integer register, so the values are its overflow
area, behind an empty register save area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… builds

sceAudioPropagationReportApi hands its report handlers the call's own
va_list, which is the x86-64 SysV one the guest handler reads only on an
x86-64 host; a lifted build for aarch64 did not compile. In lifted builds
a host caller now stops as not implemented. Guest calls already stop at
the bridge, which has no free integer register and no va_list sibling to
forward them through. The library's test leaves out its report checks,
which read the report's va_list as the host's, in lifted builds and
checks the rejection instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant