Skip to content

feat(libkernel): implement _umtx_op wait and wake operations - #1482

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
awake-devel:fix/libkernel-umtx-op
Oct 11, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
awake-devel:fix/libkernel-umtx-op

Conversation

@awake-devel

Copy link
Copy Markdown
Contributor

What

Hollow Knight: Silksong (PPSA12544) ships its own sce_module/libc.prx and Unity's Media/Modules/PS5Util.prx. Both import _umtx_op (NID 04AjkP0jO9U) from libkernel, which AnyPS5 does not export, so on Linux the relinked title stops at load with undefined symbol: 04AjkP0jO9U.

This adds _umtx_op to libkernel for the operations those modules pass, following FreeBSD 11's kern_umtx.c:

op name behaviour
2 UMTX_OP_WAIT sleep while the 64-bit value at obj equals val; return 0 at once when it differs (do_wait, compat32 = 0)
11 / 15 UMTX_OP_WAIT_UINT / _PRIVATE the same with a 32-bit comparison (do_wait, compat32 = 1)
3 / 16 UMTX_OP_WAKE / _PRIVATE wake up to val waiters; val <= 0 still wakes one (umtxq_signal_queue)

Timeouts follow umtx_copyin_umtx_time: when uaddr (the size) is at most 16, uaddr2 is a relative timespec; otherwise it is an _umtx_time, absolute with UMTX_ABSTIME on its clock. A negative second count or a nanosecond count outside [0, 1e9) fails with EINVAL, and a wait that times out fails with ETIMEDOUT. Errors are returned as -1 with the guest errno set, like the system call. A null object gives EFAULT (except WAKE_PRIVATE, which does not look up the address). Operations 0 and 1 fail with EOPNOTSUPP and operations above 26 with EINVAL, as in FreeBSD 11's op_table. The mutex, condition variable, rwlock, semaphore, shm and robust-list operations throw NotImplemented, as does a misaligned object.

Waiters use the existing sceKernelSyncOnAddressWait queue and TimedWait (no platform-specific code), so _umtx_op and sceKernelSyncOnAddressWake wake each other's waiters.

What the title calls. I disassembled the two modules:

  • PS5Util.prx: a wait helper at 0x700 (UMTX_OP_WAIT, a timespec built from a microsecond timeout) and a wake helper at 0x750 (UMTX_OP_WAKE). The result is read through __error.
  • libc.prx: helpers at 0x116b80 (UMTX_OP_WAIT_UINT), 0x116b90 (UMTX_OP_WAIT) and 0x116ba0 (UMTX_OP_WAKE). They are used by exported atomic-wait functions (name string SceLibcV2AtomicWait) and by a call_once (SceLibcIV2Once), which waits with operation 1 and wakes every waiter with UMTX_OP_WAKE(INT_MAX).

Two things in these modules disagree with FreeBSD and could not be checked without a console. Both are recorded in TechnicalDebt:

  • libc passes the WAIT operations a pointer to a 4-byte microsecond timeout, with size 0 (the layout of sceKernelSyncOnAddressWait), while PS5Util passes a timespec. With FreeBSD's rule, libc's 4-byte value is read as a timespec. The nanosecond half is then almost always out of range, so the wait returns EINVAL at once and libc's loop checks again. Only the untimed waits (null timeout) block.
  • libc's call_once waits with operation 1, which FreeBSD 11 rejects with EOPNOTSUPP. The once loop then checks the flag again until it reaches the done state, so it spins instead of sleeping.

AI-assisted: the call sites were found and this change was written with Claude Code.

This PR was investigated and written by Claude Code (AI), and posted by me after review.

Tested

Arch Linux (kernel 7.2.8), i7-8750H, GTX 1050 Ti Max-Q (NVIDIA 580.178.04), GCC 16.2.1, Release build. Each run is compared with unmodified main bc1fbe8 on the same machine. All runs set APS5_WRITE_WATCH_IMPORTS=watch; without it this NVIDIA driver creates no Vulkan device (#1289) and the GPU tests are skipped.

  • ctest -j4: 424/424 passed, 0 skipped, 0 failed (baseline 423/423, 0 skipped). The only new test is guest_umtx_op.

  • VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ctest -j2 -V: 424/424 passed. 27 Validation Error lines, the same as the baseline, with identical per-VUID counts.

  • New test core/libs/tests/GuestUmtxOp.cpp (modelled on GuestSyncOnAddress) covers:

    • a wait returning at once when the value differs;
    • 64-bit comparison for WAIT and 32-bit for WAIT_UINT;
    • relative timespec, relative _umtx_time and absolute CLOCK_MONOTONIC timeouts, including one in the past;
    • rejected timeouts;
    • a wake that releases a waiter without a value change;
    • the wake count (1, then 0 waking one, then INT_MAX), with a waiter on another address left untouched;
    • null objects, reserved and out-of-range operations, and unimplemented operations throwing.
  • Mutation check. With the test kept and the fix changed, the test failed every time:

    • fix removed: the test no longer links;
    • a wake of 0 wakes nobody: test fails;
    • UMTX_OP_WAIT compares 32 bits: test fails;
    • the timeout is ignored: test fails.

    The fix was then restored byte for byte and rebuilt, and guest_umtx_op and guest_sync_on_address passed.

  • Windows: not tested. The change has no Windows-specific code; it uses std::uint64_t for the guest u_long.

  • Silksong (PPSA12544), Linux, relinked with feat(relinker): add --module-dir for modules loaded at run time #846 (--module-dir Media/Plugins) and local throwing stubs for unrelated missing imports. On main the loader fails with undefined symbol: 04AjkP0jO9U. With this change, all modules load, the title starts, reads /app0/Media/boot.config, and stops at a later, unrelated gap: sceKernelLoadStartModule with start arguments not implemented. The log does not show whether _umtx_op ran before that point, so the wait and wake paths are covered only by the unit test.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8

@popcoder565

Copy link
Copy Markdown
Contributor

Tested on Windows 10 (19045), MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, Intel Core i7-12700H (20 logical CPUs). I merged it onto main 73ac9ef together with #1477, #1482 and #1490 to share one build.

Tested with AI assistance (Claude Code).

@fisherjoey

Copy link
Copy Markdown
Contributor

One difference from FreeBSD 11 in the WAIT_UINT path. do_wait takes u_long id, and with compat32 it reads the word with fueword32, zero-extends it into tmp and then checks tmp == id (kern_umtx.c:965-978 on stable/11). So a val with any of the high 32 bits set never matches and the call returns 0 right away.

Here the case does static_cast<std::uint32_t>(value) first, so a stored 1 with val = 0x700000001 sleeps. GuestUmtxOp.cpp:85 asserts that (ETIMEDOUT), while FreeBSD 11 would return 0 for the same call. Sign extension hits the same path: an int comparand of -1 passed as 0xffffffffffffffff against a stored 0xffffffff spins on FreeBSD and blocks here.

I checked it on the PR head (85c43cd, merged onto main 29e533a, Linux, GCC 16.2.1, Release). The PR's own guest_umtx_op passes, and a small probe gives:

stored op val timeout result here FreeBSD 11
0x1 WAIT_UINT and WAIT_UINT_PRIVATE 0x700000001 0 -1, ETIMEDOUT 0
0x1 WAIT_UINT 0x700000001 200 ms -1, ETIMEDOUT after 200 ms 0, no sleep
0xffffffff WAIT_UINT 0xffffffffffffffff 200 ms -1, ETIMEDOUT after 200 ms 0, no sleep

It doesn't matter much in practice, and I can't say what the PS5 kernel does. Since the description says this follows do_wait, though, the minimal change is to pass the full value to UmtxWait, return 0 when expected > std::numeric_limits<TValue>::max() (after the timeout/null/alignment checks, to keep the error order), and flip line 85 to expect 0.

AI-assisted: yes

@oneandonlydean

Copy link
Copy Markdown
Collaborator

For the two cases where PPSA12544's own libc.prx doesn't match FreeBSD 11 (a 4-byte microsecond timeout with size 0 on the WAIT ops, and operation 1 in its call_once), please throw instead of returning EINVAL/EOPNOTSUPP in SyncOnAddress.cpp. The title's libc shows the console handles those calls some other way, and returning FreeBSD's error makes it spin. Unknown behaviour throws here. The rest can go in.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed write-up. Two things before this can go in:

  • As @oneandonlydean asked, throw (NotImplemented_nid_no_patch) instead of returning an error in the two cases where the title's libc.prx disagrees with FreeBSD 11: the 4-byte microsecond timeout with size argument 0 on the WAIT ops, and operation 1 (SyncOnAddress.cpp:250-254, UmtxDeadline at :128). Please also say how a libc call is told apart from PS5Util's timespec call if both pass size 0; if they can't be told apart, say so in TechnicalDebt and throw only on what is certain.
  • As @fisherjoey noted, FreeBSD's do_wait with compat32 compares the zero-extended word against the full val, so WAIT_UINT with any high bit set in val returns 0 at once. SyncOnAddress.cpp:243 truncates to 32 bits, and GuestUmtxOp.cpp:83 asserts the opposite. Pass the full value and return 0 when it doesn't fit (after the timeout/null/alignment checks), and flip that assertion.

Update the TechnicalDebt line (TechnicalDebt.md:132) to match. Happy to look again once pushed.

@GustavoGamarra95

Copy link
Copy Markdown
Contributor

Another title this unblocks on Linux: God of War Sons of Sparta (PPSA28997, Unity IL2CPP).

On main the loader stops at undefined symbol: 04AjkP0jO9U from Media/Modules/PS5Util.prx. With head 85c43cd applied (on main 942a174, together with #1431 for sceKernelRaiseException), all modules load, and the title boots and plays through several levels, with saves and trophies working.

_umtx_op runs a lot there. During gameplay the main thread and another Unity thread were inside _umtx_op → WaitOnAddressUntil, called from PS5Util.prx, in 38 and 36 of 40 eu-stack samples. Those are the frame waits between Unity's threads, so the wait and wake path is exercised continuously. I saw no hang or lost wake-up over several play sessions, the longest about ten minutes.

@overlordxrz-source

Copy link
Copy Markdown
Contributor

For priority, an update from a catalog run on main 6c9cb3a (Linux). _umtx_op (04AjkP0jO9U) is the first stop for these 9 titles:

  • VITA5 (PPSA39410)
  • Stremio Plus (PPSA74126)
  • ProsperoLight (PPSA99002)
  • ProsperoTV (PPSA99003)
  • PS5 OpenGL Showcase (PPSA99005)
  • ProsperoPuzzles (PPSA99006)
  • ProsperoLichess (PPSA99009)
  • Homebrew UI Lab (PPSA99050)
  • Forwarder Manager (PPSA99930)

Two more stop here once the relinker gets past their SDK syscall stubs with the opt-in flag discussed in #2136: OliSe (PPSA01153) and PS5CEMU-HAR (PPSA99360). That makes 11.

No rush, @awake-devel. Thanks for the PR.

@LeandroLP23

Copy link
Copy Markdown
Contributor

Windows data point, two more catalog titles. Head 85c43cde merged onto main f60923b, Windows 11 (26300), WinLibs GCC 15.2.0 r7, Release, titles relinked with their own libc.prx unwrapped:

Title main with this PR
PS5 OpenGL Showcase (PPSA99005) FAIL: unresolved ELF import 04AjkP0jO9U from libkernel.prx gets past it to 7Psx1DlAyE4 (getprogname, #3347)
ProsperoRadio (PPSA99001) same same

guest_umtx_op passes 3/3 here. With #3347 on top as well, the OpenGL showcase loads, brings up Mesa and stops in the shader recompiler (#2631), and ProsperoRadio stops at setcontext.

awake-devel and others added 3 commits October 11, 2026 11:13
Titles that ship their own libc.prx or Unity's PS5Util.prx import
_umtx_op (04AjkP0jO9U) for atomic waits, call_once and futex-style
waits. Implement UMTX_OP_WAIT, UMTX_OP_WAKE, UMTX_OP_WAIT_UINT,
UMTX_OP_WAIT_UINT_PRIVATE and UMTX_OP_WAKE_PRIVATE as FreeBSD 11's
kern_umtx.c does, on the sceKernelSyncOnAddressWait waiter queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
@adriadam10
adriadam10 force-pushed the fix/libkernel-umtx-op branch from 85c43cd to 3879e7e Compare October 11, 2026 09:22

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks all for the testing. Pushed the review fixes on top of your commit:

  • UMTX_OP_WAIT_UINT/_PRIVATE now pass the full val and return 0 at once when it doesn't fit in 32 bits (FreeBSD's zero-extended compare), after the timeout/null/alignment checks; test flipped. Commit by @babyhuey (help/umtx-op-1482), cherry-picked.
  • Operation 1 throws NotImplemented_nid_no_patch; operation 0 keeps EOPNOTSUPP.
  • A timeout with size argument 0 that isn't a valid timespec (negative seconds, nanoseconds outside [0, 1e9)) throws instead of returning EINVAL. A libc microsecond value can't be told apart from a timespec by the size alone, so a value that reads as a valid timespec is waited on as one; this is in TechnicalDebt. EINVAL tests now pass a size.

Rebased on main; guest_umtx_op and guest_sync_on_address pass; the test no longer links without the change.

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.

10 participants