Skip to content

libkernel: threads of a PS5 payload process start with MXCSR 0x9fc0 (FTZ and DAZ); AnyPS5 does not set it on guest threads #7548

Open

Description

@ionut27001

AnyPS5 does not set the MXCSR that threads of a PS5 payload process start with (0x9fc0: FTZ and DAZ set)

In a payload process on a PS5 (firmware 12.70), every thread I measured started with MXCSR = 0x9fc0. That is the default 0x1f80 with flush-to-zero (bit 15) and denormals-are-zero (bit 6) also set. FreeBSD starts threads with 0x1f80 (__INITIAL_MXCSR__ in sys/x86/include/fpu.h). The x87 control word was 0x37f.

This value does not depend on the creating thread. After the main thread set its own MXCSR to 0x1f80, the threads it created next with pthread_create and scePthreadCreate still started at 0x9fc0. So in this payload process a new thread does not take its MXCSR from its creator.

I could not find any place on main where AnyPS5 sets MXCSR for a guest thread:

  • Created threads: scePthreadCreate in core/libs/prx/libkernel/Pthread/src/Thread.cpp starts guest threads with _beginthreadex on Windows and pthread_create elsewhere, with StartNativeThread as the start routine. The guest pthread_create goes through it too (Pthread/Posix/Thread.cpp). Neither function sets MXCSR, so a guest thread keeps the MXCSR the host gives it.
  • The main thread: the guest's main thread does not go through StartNativeThread. It is the host process's initial thread. The relinker's entry stub calls the ELF entry on it (core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp, core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp), and nothing under core/relinker sets MXCSR either. I did not measure its value under AnyPS5.
  • Saved and restored, never set: JumpBuffer.cpp, UserContext.cpp, Exception.cpp and libSceFiber/Export.cpp save and restore MXCSR, but none sets a starting value for a thread. A new fiber starts with the MXCSR of the thread that first runs it (PrepareInitialStack reads it with _mm_getcsr).

What the host gives a new thread:

  • Windows: 0x1f80. In a host test (MinGW-w64 GCC 15.2.0, Windows 11), the test program's main thread started at 0x1f80. After it set 0x9fc0, a _beginthreadex thread and a std::thread both still read 0x1f80.
  • Linux: "The new thread inherits the calling thread's floating-point environment" (pthread_create(3)). So a guest thread starts with its creator's MXCSR, and a thread created by a thread that set 0x9fc0 would start at 0x9fc0. I did not run this on Linux.

The x87 control word is covered by the existing asinl entry in docs/dev/debt/libc.md, which records that guest threads do not set it either.

Without FTZ and DAZ, subnormal results are kept instead of flushed, and subnormal inputs are used instead of read as zero.

What the console's own libSceLibcInternal.sprx returns under each MXCSR. These are the console's functions, not AnyPS5's (see the resolve lines). AnyPS5's strtod calls the host's std::strtod (Strings.cpp), so what it returns under 0x9fc0 would depend on the host C library.

  • strtod("4.9e-324") returns 0 with ERANGE under 0x9fc0. With MXCSR forced to 0x1f80 it returns 0x1, still with ERANGE.
  • strtof("1.17549435e-38") returns 0 with ERANGE under 0x9fc0. With 0x1f80 it returns 0x00800000 and leaves errno at 0.
p15: case fp_main mxcsr=0x9fc0 fcw=0x37f
p15: case fp_pthread create_ret=0 mxcsr=0x9fc0 fcw=0x37f
p15: case fp_scePthreadCreate create_ret=0 mxcsr=0x9fc0 fcw=0x37f
p15: case fp_pthread_after_main_set_1f80 create_ret=0 mxcsr=0x9fc0 fcw=0x37f
p15: case fp_scePthreadCreate_after_main_set_1f80 create_ret=0 mxcsr=0x9fc0 fcw=0x37f
p15: resolve sym=scePthreadCreate module=libkernel_sys.sprx exported=yes
p14: meta mxcsr_at_start=0x9fc0
p14: resolve sym=strtod module=libSceLibcInternal.sprx same_as_called=yes
p14: resolve sym=strtof module=libSceLibcInternal.sprx same_as_called=yes
p14: case strto text="4.9e-324" strtod=000000000000000000 used_d=8 errno_d=34 strtof=0000000000 used_f=8 errno_f=34 mxcsr=0x9ffd
p14: case strto_mxcsr1f80 text="4.9e-324" strtod=0x0000000000000001 used_d=8 errno_d=34 strtof=0000000000 used_f=8 errno_f=34 mxcsr=0x1fb2
p14: case strto text="1.17549435e-38" strtod=0x380fffffff9fdba8 used_d=14 errno_d=0 strtof=0000000000 used_f=14 errno_f=34 mxcsr=0x9ffd
p14: case strto_mxcsr1f80 text="1.17549435e-38" strtod=0x380fffffff9fdba8 used_d=14 errno_d=0 strtof=0x00800000 used_f=14 errno_f=0 mxcsr=0x1fb2

The low six bits of each mxcsr= value are sticky exception flags. The probe does not clear them between cases, so they include flags raised by earlier calls. The control bits are 0x9fc0 and 0x1f80.

For some inputs, the console's nextafter and erfcf, from the same module, also depend on MXCSR:

  • nextafter(0.0, 1.0) returns 0 under the start MXCSR and the smallest subnormal under 0x1f80.
  • erfcf(10.0f) returns 0 under the start MXCSR and 0x00000002 under 0x1f80.

Not every subnormal result changes. nextafter(DBL_MIN, 0.0) returns the largest subnormal, 0x000fffffffffffff, under both values, and tgamma(-171.5) returns the same subnormal, 0x0000238ee05c87a0, under both:

p20: resolve sym=nextafter module=libSceLibcInternal.sprx exported=yes
p20: resolve sym=erfcf module=libSceLibcInternal.sprx exported=yes
p20: resolve sym=tgamma module=libSceLibcInternal.sprx exported=yes
p20: case nextafter mode=start x=000000000000000000 y=0x3ff0000000000000 result=000000000000000000 errno=0 fe=0
p20: case nextafter mode=mxcsr1f80 x=000000000000000000 y=0x3ff0000000000000 result=0x0000000000000001 errno=0 fe=0x32
p20: case erfcf mode=start x=0x41200000 result=0000000000 errno=0 fe=0x30
p20: case erfcf mode=mxcsr1f80 x=0x41200000 result=0x00000002 errno=0 fe=0x32
p20: case nextafter mode=start x=0x0010000000000000 y=000000000000000000 result=0x000fffffffffffff errno=34 fe=0x30
p20: case nextafter mode=mxcsr1f80 x=0x0010000000000000 y=000000000000000000 result=0x000fffffffffffff errno=34 fe=0x30
p20: case tgamma mode=start x=0xc065700000000000 result=0x0000238ee05c87a0 errno=0 fe=0x30
p20: case tgamma mode=mxcsr1f80 x=0xc065700000000000 result=0x0000238ee05c87a0 errno=0 fe=0x30

The pNN: prefix names the probe; the rest of each line is copied unchanged.

Not established:

  • These threads ran in a homebrew payload process started through a payload manager, not in a game process. In this payload process a new thread's MXCSR does not depend on its creator, which suggests the console's thread-creation path sets it, but I have not read MXCSR inside a game.
  • p14's lgamma of ±2^-1074 returned 0x40875427f6acad6f (about 746.52) where -log(|x|) is about 744.44. It ran only under the start MXCSR, so whether that difference comes from MXCSR is not measured.
  • Whether AnyPS5 should set 0x9fc0 on guest threads is for the maintainers to decide. AnyPS5's own host code (libraries, CRT, drivers) also runs on those threads, and host libraries may expect IEEE denormals.

Method:

  • The console runs firmware 12.70 and is jailbroken.
  • The probes are payloads built with ps5-payload-dev/sdk v0.44 and clang 18.1.8.
  • MXCSR was read with stmxcsr and the x87 control word with fnstcw. This was done in the main thread and in threads made with pthread_create (the payload's import) and with scePthreadCreate, which was resolved from libkernel_sys.sprx.
  • p14 calls the payload's strtod, strtof and lgamma. Its resolve lines show that all three are the libSceLibcInternal.sprx exports. p20 calls nextafter, erfcf and tgamma through pointers resolved from that module.
  • The probe sources and logs are attached: mxcsr-probes.zip.

AI-assisted.

Activity

  1. MarcZX10 commented on Oct 18, 2026

    @MarcZX10
    Contributor

    I checked the current thread path: StartNativeThread calls RunThread on the native host thread, where guest execution and emulator host work share that thread. Setting MXCSR once at thread start would therefore affect host-side work too. Before a patch, could you confirm whether a game-process thread was measured (the report currently says it was not), and whether the console applies 0x9fc0 only while guest code runs or for the full thread lifetime? That determines whether this belongs at guest-entry boundaries or in native thread initialization.

  2. pushkarreddyy commented on Oct 18, 2026

    @pushkarreddyy
    Contributor

    I am taking this up to implement setting MXCSR to 0x9fc0 (FTZ and DAZ) at the Aps5CallGuestEntry_nid_no_patch guest-entry boundary in core/libs/prx/libkernel/Pthread/src/Thread.cpp. This preserves host MXCSR across CRT and driver threads while ensuring all guest execution starts under the measured console hardware state (0x9fc0). A focused test verifying guest MXCSR state and host preservation will be included.

  3. ionut27001 commented on Oct 18, 2026

    @ionut27001
    ContributorAuthor

    @MarcZX10 No game process was measured; everything here comes from a homebrew payload process, as the "Not established" section says. In that process there is no host-side code, so the console cannot show a difference between "only while guest code runs" and "the whole thread". What the logs do show:

    • A new thread reads 0x9fc0 as the first thing its entry function does, for both pthread_create and scePthreadCreate (p15 posix_thread).
    • The main thread reads 0x9fc0 after the probe's own setup code has run (oracle_begin, before the p14 and p15 reads).
    • The control bits are still 0x9fc0 after calls into libSceLibcInternal.sprx: the p14 strto lines read 0x9ffd, which is 0x9fc0 plus sticky flags.

    Not measured: MXCSR after a blocking syscall, inside a signal handler, or in a game process.

    @pushkarreddyy Two things from the current code that may matter for the guest-entry approach:

    • Aps5CallGuestEntry_nid_no_patch is only called from RunThread, so it covers threads created through scePthreadCreate (and the guest pthread_create, which goes through it). The guest main thread does not pass through it: the relinker's entry stub (core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp, core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp) calls the ELF entry directly, and the main thread is where p14 and p15 also read 0x9fc0.
    • HLE functions that guest code calls run on the same thread, so with a boundary at guest entry they would run under 0x9fc0 too; only the host work around the trampoline (thread setup and finishThread) would keep the host value, if MXCSR is restored after the guest entry returns.
  4. jotitadev commented on Oct 18, 2026

    @jotitadev
    Contributor

    macOS host data for the guest-entry approach in #7565: Rosetta 2 honours 0x9fc0 the way the console value needs, so the same ldmxcsr works on the macOS port, where guest code runs as x86_64 under Rosetta.

    Measured on an M2 Max, macOS 27.2, with a standalone x86_64 probe (Apple clang, -O1, and again with -mavx2) that sets MXCSR with ldmxcsr and runs the instructions through inline asm:

    MXCSR set readback DAZ: comiss denormal vs 0 is equal DAZ: addss/addsd denormal + 0 DAZ: cvtss2sd denormal FTZ: FLT_MIN * 1/3 (mulss, vmulps), DBL_MIN * 1/3 (mulsd)
    0x1f80 0x1f80 no denormal kept 0x37a2345000000000 denormal (0x002aaaab, 0x0005555555555555)
    0x9f80 (FTZ) 0x9f80 no 0 0x37a2345000000000 0
    0x1fc0 (DAZ) 0x1fc0 yes 0 0 denormal
    0x9fc0 0x9fc0 yes 0 0 0

    The sticky bits after the run are what x86 sets (DE, UE, PE: 0x1fb2 without DAZ, 0x1ff0 with it), and the AVX vaddps/vmulps rows match the SSE ones. So FTZ and DAZ are applied separately for f32 and f64, scalar, SSE packed and AVX.

    The rest of the thread lifetime, with 0x9fc0 set on the thread first:

    • raise(SIGUSR1): the handler runs with 0x9fc0. After it returns, the thread is back to 0x9fc0, even though the handler had loaded 0x1f80.
    • A write fault on a read-only page, with a SIGSEGV/SIGBUS handler that calls mprotect and returns: the same result.
    • usleep: 0x9fc0 is kept.
    • pthread_create from that thread: the new thread starts with 0x1f80. On this host a new thread does not take its creator's MXCSR, so it gets 0x9fc0 only if something loads it on that thread, as Aps5CallGuestEntry_nid_no_patch does in feat(libkernel): initialize created guest thread MXCSR to 0x9fc0 at entry boundary #7565. As @ionut27001 noted, the guest main thread does not go through that function.

    Not run: AnyPS5 itself. main does not build in full on macOS yet, so this covers only the host behaviour that the #7565 approach relies on.

    Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

  5. MarcZX10 commented on Oct 18, 2026

    @MarcZX10
    Contributor

    I opened #7594 for the separate main guest ELF entry path. It initializes MXCSR to 0x9fc0 immediately before guest entry in the Linux and Windows startup stubs and restores the host MXCSR if guest entry returns. This complements #7565, which covers created guest threads. The relinker-only Linux build and all 198 registered CTest tests passed; the Windows-generated stub is byte-checked on Linux, with no Windows runtime test. This does not cover the separate macOS launcher path or close this issue.

  6. pushkarreddyy commented on Oct 18, 2026

    @pushkarreddyy
    Contributor

    Thanks @MarcZX10! That fits cleanly together: #7594 handles the relinker entry stubs for the main guest thread, while #7565 covers created guest threads via Aps5CallGuestEntry_nid_no_patch and preserves 0x9fc0 across finishThread destructors before restoring the worker's host MXCSR in RunThread.

  7. zaryabzip commented on Nov 4, 2026

    @zaryabzip
    Contributor

    Correction: @LeandroLP23 raised this x87 gap first, on #7565 (comment). @LeandroLP23, if you're planning the follow-up, it's yours and I'll drop this.

    Their Windows probe (WinLibs GCC 15.2) narrows the scope:

    If nobody else takes it, I'll do the follow-up after #7565 lands:

    • fldcw 0x37f beside its ldmxcsr in Aps5CallGuestEntry_nid_no_patch;
    • the worker's own control word saved and restored at the same points as its MXCSR;
    • a guest_pthread_start check that a scePthreadCreate thread reads 0x37f with fnstcw, which fails on main on Windows.

    AI-assisted: yes (Claude Code)

  8. LeandroLP23 commented on Nov 4, 2026

    @LeandroLP23
    Contributor

    Thanks @zaryabzip, it's yours. I'm not working on it, and your plan covers it: fldcw 0x37f next to the ldmxcsr in Aps5CallGuestEntry_nid_no_patch, the worker's control word saved and restored with its MXCSR, and the fnstcw check in guest_pthread_start.

    When it's up, I can run that check on Windows (WinLibs GCC 15.2, Radeon 860M laptop), on main and on your branch, to confirm it fails before the fix and passes after.

    AI-assisted: yes (Claude Code)

  9. zaryabzip commented on Nov 4, 2026

    @zaryabzip
    Contributor

    Thanks @LeandroLP23. I'll open it once #7565 has merged and tag you then for the main-versus-branch run.

    AI-assisted: yes (Claude Code)

  10. zaryabzip commented on Nov 4, 2026

    @zaryabzip
    Contributor

    Correction to my plan above: created guest threads are already done on main. @AlexFigas's 8b626e3074 (Oct 29) loads 0x037f in RunThread (GUEST_THREAD_FPU_CONTROL, Thread.cpp:66 and :901), with a test in GuestKernelSysThreadDefaults.cpp. I missed it, so there is nothing to add next to #7565's ldmxcsr.

    What may still be open is the main thread on Windows:

    So the only possible follow-up is fldcw 0x37f in the Windows entry stub, next to #7594's MXCSR line. Before claiming that, I'll measure a relinked title's first guest instruction on Windows. If it already reads 0x37f, there is nothing to do here.

    AI-assisted: yes (Claude Code)

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions