Skip to content

feat(libkernel): run threads for a lifted guest - #13

Open
oberonlift wants to merge 5 commits into
lifted-libc-long-doublefrom
lifted-libkernel
Open

oberonlift wants to merge 5 commits into
lifted-libc-long-doublefrom
lifted-libkernel

Conversation

@oberonlift

@oberonlift oberonlift commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

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

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 7 of 8 PRs that let AnyPS5's libraries run a lifted program; the lifter itself follows them.

x86-64: behaviour unchanged. Thread objects stay on the host heap; the stack size arithmetic shared with lifted builds computes the same values.

What

  • Guest thread entries and exception handlers are called through GuestCpu, and a lifted thread entry, which the bridge already turned into a native thunk, is called directly. The x86 assembly for sigsetjmp, siglongjmp, getcontext and their kin is left out of lifted builds, where the runtime provides the jumps and context switching reports itself as not implemented.
  • A lifted guest thread runs on a guarded guest stack of the size it asked for, handed to the runtime through GuestCpuUseStack and reported by scePthreadAttrGet; the host stack is the size GuestCpuHostStackSize gives. The thread takes its guest stack and the console's x87 control word before it unblocks signals, so a signal raised at a new or suspended thread runs its handler on that guest stack.
  • In lifted builds thread objects come from the guest heap, so every thread handle is a guest address, as on the console. On an aarch64 host the host heap lies outside anything the guest can address.
  • DoMprotect reports the pages each protection change makes writable to GuestCpuMakingWritable_nid_no_patch. ps5lift loads from read-only segments without ordering, and its runtime stops the program when such pages overlap one. A MAP_FIXED mapping over such a segment is not reported yet.
  • A guest exception handler runs through GuestCpuCallInterrupted and sees the guest registers that GuestCpuInterruptedContext reports; the signal-delivery assembly is left out of lifted builds.
  • sceKernelWaitOnAddress reads a misaligned value with a plain copy followed by an acquire fence instead of x86's mov.
  • EnterGuestMainStack calls the guest entry on the stack the runtime gave the main thread, instead of switching to a main stack at the console's address.
  • The libkernel debt file lists what still differs in lifted builds, among them the MAP_FIXED gap, the host stack that the main and adopted threads report, and the zero fpstate, cs and ss an exception handler reads.

boykopovar#5021 also changes some of the same files: Thread.cpp, Pthread.hpp, Exception.cpp, SignalSets.cpp, DirectMemory.cpp.

Tested

Compiled for x86-64 Linux before and after this PR (GCC 13, same flags): 153 of the 155 objects it reaches are byte-identical; Thread.cpp differs in scePthreadCreate's register allocation and a line-number constant, and Exception.cpp in two line-number constants.

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): keep the guest's x87 long double exactly in lifted builds #12
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 68aea15 to 2c7c036 Compare November 1, 2026 19:35
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 2c7c036 to b7eea78 Compare November 1, 2026 22:07
@oberonlift
oberonlift force-pushed the lifted-libkernel branch 2 times, most recently from 61186a9 to 319ad33 Compare November 2, 2026 17:02
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch 2 times, most recently from 2c34c7a to a1cc1f9 Compare November 2, 2026 18:30
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from a1cc1f9 to 84e7323 Compare November 2, 2026 22:07
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 84e7323 to e405a49 Compare November 3, 2026 16:34
@oberonlift

Copy link
Copy Markdown
Owner Author

Review findings (unverified)

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

  1. core/libs/prx/libkernel/Pthread/src/Thread.cpp:943: signals are unblocked before the guest stack is set up. RunThread calls pthread_sigmask (line 939) before GuestCpuUseStack. A raised exception already pending, or sent by sceKernelRaiseException to a suspended thread, is delivered in that gap. Deliver → FromGuest → GuestCpuInterruptedContext → ps5lift_current_state() then starts the thread as a foreign guest thread. The later ps5lift_use_guest_stack fails with "a thread that already runs guest code cannot move to another guest stack". x86 sets fldcw before unblocking, so moving UseStack and SetX87Control ahead of the unblock closes the gap.
  2. core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp:1502: DoMprotect reports the unrounded range. mprotect changes the page-rounded [first, end), but GuestCpuMakingWritable gets [address, address+len). A read-only segment that ends partway into a 16 KiB page becomes writable when the guest protects a few later bytes of that page, and the runtime is not told. On __APPLE__ the range is widened further and the report stays narrow. Pass (first, bytes).
  3. core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp:867: only DoMprotect reports writable ranges. A writable MAP_FIXED mapping over an image segment (RemapFixedIntoRegistered, MapAnonymous, DoMapFileBacked_nid_no_patch, DoMapDirect, DoMapShared) never reaches GuestCpuMakingWritable. Hooking where writable state is recorded (GuestAllocations::Mutation::Protect/Add with writable = true) would cover every path.
  4. core/libs/prx/libkernel/Pthread/src/Thread.cpp:1110: a lifted thread on a non-Linux POSIX host gets a null guest stack. AllocateGuestStack and RetireGuestStack exist only under __linux__. Elsewhere guestStack is null and guestStackBytes 0, so stackAddress wraps around and GuestCpuUseStack gets stackTop = 0. The old "Cannot query guest thread stack" check is gone, and nothing reports the build as unsupported.
  5. core/libs/prx/libkernel/Pthread/src/Thread.cpp:1413: adopted threads describe a host stack. Adopted threads, the main thread included, still take their stack from SetStackFromHost, and EnterGuestMainStack never sets guestMainStackLow/High. So scePthreadAttrGet(scePthreadSelf()) returns a 0xffff… host stack the guest never runs on. DoMprotect's stack-guard logic never matches the real guest main stack either.
  6. core/libs/prx/libkernel/System/src/Exception.cpp:192: the guest handler gets a ucontext on the host stack. The lifted CallHandler passes &ucontext from the host signal frame, outside guest-addressable memory. This is the problem the PthreadPrivate guest-heap commit fixes for thread handles. Build the context on the interrupt stack or the guest heap.
  7. core/libs/prx/libkernel/System/src/Exception.cpp:930: FromGuest fills only the general registers, rip and rflags. fpstate (MXCSR, the x87 control word, the XMM registers) and cs/ss stay zero. A handler reading mcontext.fpstate sees every SSE exception unmasked, and CallHandler's mask fix-up runs on a zero mask. docs/dev/debt/libkernel.md mentions only the registers.
  8. core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp:191: the unaligned LoadAddressValue has no acquire ordering. It is a plain memcpy between atomic_signal_fence calls, which order only the compiler. On aarch64 a waiter that sees the new value can still read stale data the waker wrote before its store, so an atomic_thread_fence(acquire) is needed after the load. The unaligned 8-byte load can also tear across a 16-byte boundary.
  9. core/libs/prx/libkernel/Pthread/src/Thread.cpp:246: the lifted EnterGuestMainStack dropped the "already entered" guard. A second call runs the guest entry again where x86 throws. guestMainStackLow/High are atomics nothing writes, GUEST_MAP_FIXED is unused (clang warns with -Wunused-const-variable), and the HostStackLimits main-stack branch can never match in lifted builds.
  10. core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp:1502: every writable DoMprotect scans every segment of every image under the caller's locks. That includes internal callers that can never touch image segments, such as spare-stack reuse in AllocateGuestStack and the per-command AMM changes in Apr.cpp. A sorted list of read-only segments in the runtime, or reporting only from guest-facing entry points, would avoid it.

@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from e405a49 to 3e77693 Compare November 4, 2026 15:40
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 3e77693 to c9c7bc4 Compare November 4, 2026 16:20
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from c9c7bc4 to 093bf45 Compare November 4, 2026 17:17
@oberonlift
oberonlift force-pushed the lifted-libkernel branch 2 times, most recently from 2a44260 to c0cdf08 Compare November 4, 2026 20:18
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch 2 times, most recently from 524b8de to 3bf0e50 Compare November 5, 2026 01:21
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 3bf0e50 to a68d0af Compare November 5, 2026 15:34
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from a68d0af to 3bee83c Compare November 5, 2026 19:28
@oberonlift
oberonlift force-pushed the lifted-libc-long-double branch from 3bee83c to ccbc82d Compare November 6, 2026 00:18
oberonlift and others added 5 commits November 6, 2026 20:42
Call guest thread entries and exception handlers through GuestCpu, and
leave out the x86 assembly for sigsetjmp, siglongjmp, getcontext and their
kin in lifted builds, where the runtime provides the jumps and context
switching reports itself as not implemented. A handler raised on a lifted
thread sees the guest registers of the thread's last call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bridge already turns the guest entry point into a native thunk, so
a lifted build calls it directly instead of through a GuestCpu call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lifted thread's guest frames lived on ps5lift's fixed 8 MiB stack
while scePthreadCreate sized the host stack, which holds the lifted
frames, from the guest's request and reported part of it as the
thread's stack. The guest stack is now a guarded mapping of the size
the guest asked for, which the thread hands to the runtime through
GuestCpuUseStack before its entry runs and which scePthreadAttrGet
reports, and the host stack is the size GuestCpuHostStackSize gives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thread handle is the address of its PthreadPrivate, which came from
the host heap. On the console it is the address of the thread's
structure in the process's own memory. On an aarch64 host the host
heap lies at 0xaaaa... and 0xffff..., outside anything the guest can
address, and a library that prints a handle with %p and reads it back
with base 0 saw a leading hex letter and threw where the console's
handle starts with a digit.

In lifted builds PthreadPrivate now comes from the guest heap, so every
handle, those of adopted threads included, is a guest address. Direct
builds keep the host heap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ps5lift loads from a lifted image's read-only segments without ordering,
which holds only while nothing writes them. In lifted builds DoMprotect
reports the pages each protection change makes writable to
GuestCpuMakingWritable_nid_no_patch, and the runtime stops the program
when they overlap such a segment. A MAP_FIXED mapping over such a
segment is not reported; the libkernel debt notes record it.

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