Skip to content

fix(libkernel): fail scePthreadMutexUnlock on a statically initialized mutex with EPERM - #8617

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
Lindamea1:fix/libkernel-mutex-unlock-static-eperm
Oct 23, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
Lindamea1:fix/libkernel-mutex-unlock-static-eperm

Conversation

@Lindamea1

Copy link
Copy Markdown
Contributor

What

On main, scePthreadMutexUnlock resolves the slot with resolveMutex(mutex, false), which throws while the slot still holds the static initializer (0) or the adaptive static initializer (1). Unlocking a statically initialized mutex that was never locked ends the process:

[libc] unhandled exception 'St13runtime_error' thrown from 0x7f030b247cd4
terminate called after throwing an instance of 'std::runtime_error'
  what():  Mutex is not initialized

No title is known to reach this.

FreeBSD 11.4's _pthread_mutex_unlock passes the slot value to mutex_unlock_common without initializing it, and mutex_unlock_common returns EINVAL for the destroyed marker (2) and EPERM for the two initializers (NULL and 1, thr_private.h). #6244 added the EINVAL half. shadPS4 registers scePthreadMutexUnlock (tn3VlD0hG60) as ORBIS(posix_pthread_mutex_unlock), which has the same guard. Here pthread_mutex_unlock and scePthreadCondWait already answer EPERM for these slots.

The change: scePthreadMutexUnlock loads the slot once without initializing it, as mutexOwnership in Cond.cpp does. The destroyed marker still returns 0x80020016. The two initializers return 0x80020001 and stay in the slot, so a later lock still creates the mutex (an adaptive one for 1). A null mutex pointer still throws, and a live mutex answers as before. resolveMutex(..., false) has no caller left; the parameter stays because open #8400 adds resolveMutex(mutex, true) callers.

Not measured on a console.

Tested

Gentoo Linux, kernel 7.2.8, GCC 16.2.1, glibc 2.43, Ninja, Release, BUILD_TESTING=ON, on main 989b6ea; after rebasing onto main b85d972, guest_pthread_destroyed_objects, guest_pthread_mutex_errors and guest_pthread_mutexattr pass again (5 of 5 runs).

  • guest_pthread_destroyed_objects (extended): scePthreadMutexUnlock on a slot holding 0 returns 0x80020001 and leaves 0; the slot then locks and unlocks (0), a second unlock returns 0x80020001 and destroy returns 0. On a slot holding 1 it returns 0x80020001 and leaves 1; lock, unlock and destroy then return 0. Passed 20 runs in a row.
  • Negative control: with Mutex.cpp from main and the new test kept, the test aborts at its first new assertion with the Mutex is not initialized lines above. A probe linked against that libkernel.prx shows that the slot holding 1 throws the same way. Against this branch both slots return 0x80020001 and stay unchanged, and the destroyed slot (0x80020016), an unlocked mutex and one held by another thread (0x80020001) and one held by the caller (0) answer as on main.
  • Tests the change reaches, selected by tools/select_tests.py as CI selects them (1416 of 1716): 1400 passed, 0 failed, 16 skipped. The skipped ones are the 13 video_out_flip_* tests, video_out_frame_dump_queue and video_out_telemetry_caption (no display or Vulkan device under SDL's offscreen driver) and agc_driver_stencil_ref_export (the device lacks VK_EXT_shader_stencil_export). They skip on this machine on main too; video_out_telemetry_caption was checked with main's Mutex.cpp.
  • Windows: x86_64-w64-mingw32-g++ 16.2.0 -fsyntax-only -Wall -Wextra on the two changed .cpp files: no warnings, no errors. This is a syntax check, not a Windows build or run.
  • tools/check_compile.py --base upstream/main: the 2 changed translation units compile. tools/check_conventions.py passes.
  • tools/pr_overlap.py --branch fix/libkernel-mutex-unlock-static-eperm: no overlap with open pull requests.

Not run on Windows, macOS or a console.

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: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

🤖 Generated with Claude Code

…d mutex with EPERM

scePthreadMutexUnlock resolved the slot with resolveMutex(mutex, false),
which throws "Mutex is not initialized" while the slot still holds the
static initializer (0) or the adaptive static initializer (1), so
unlocking a statically initialized mutex that was never locked ended
the process. FreeBSD 11.4's mutex_unlock_common returns EPERM for both
without initializing the slot, and EINVAL for the destroyed marker.

Load the slot once: the destroyed marker still returns 0x80020016, and
the two initializers now return 0x80020001 and stay in the slot, so a
later lock still creates the mutex.

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

xXPBSXx commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Independent Windows run of this PR (data only).

Environment: Windows 10 Pro 10.0.19045, Ryzen 7 5800X3D, WinLibs GCC 15.2.0 r7 (MinGW-w64 UCRT), Release, BUILD_TESTING=ON. Merge-tree of the PR head against main 339217e is clean. I built only guest_pthread_destroyed_objects_tests; ctest -N -R destroyed lists exactly one test.

Results (3 runs each):

  • PR head: guest_pthread_destroyed_objects passes 3/3.
  • Baseline: I kept the PR's test and replaced only core/libs/prx/libkernel/Pthread/src/Mutex.cpp with its main version, then rebuilt. The test fails 3/3 (exit 0xc0000409, a failed Require). I restored the PR's Mutex.cpp afterwards.

Not run: other platforms, the other pthread tests that call scePthreadMutexUnlock, the full repository CTest, and any title or console run.

AI-assisted (Claude Code).

@ycbuzi

ycbuzi commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Windows run of c68ffcf merged onto main aa40867 (a26da68237, clean). Windows 11 Home 10.0.26200, MinGW-w64 GCC 15.2.0 (ucrt-posix-seh r7), Release, BUILD_TESTING=ON.

ctest -R "^guest_pthread_(destroyed_objects|mutex_errors|mutexattr)$" --repeat until-fail:5: all three pass every repeat. Not run: the negative control with main's Mutex.cpp.

@Lindamea1

Copy link
Copy Markdown
Contributor Author

Checked the overlap note by hand. #8373's only change to Pthread/src/Mutex.cpp is removing scePthreadMutexattrSetprotocol, which it re-adds in Posix/Mutex.cpp beside its new accessors. This PR only changes scePthreadMutexUnlock there. With #8373's head b99eaee7e1 merged on top of main d6e7ab42df plus this PR, src/Mutex.cpp merges without conflict. #8373's only conflict is its own one with main in docs/dev/debt/libkernel.md. So no order is needed between them.

AI-assisted: yes (Claude Code).

@oneandonlydean oneandonlydean self-assigned this Oct 22, 2026
@ntamas94

Copy link
Copy Markdown
Contributor

A related console data point, not the same case: measured on a PS4 Pro (firmware 10.71, 10.710.001) with a user-mode homebrew PRX loaded by a small watcher app, on mutexes made with scePthreadMutexInit; one line per call as function[label] | arguments | return | errno | output:

scePthreadMutexUnlock | &$mtx | 0x00000000 | - |
scePthreadMutexUnlock[not-held] | &$mtx | 0x80020001 | - |
scePthreadMutexUnlock[rec-1] | &$rmtx | 0x00000000 | - |
scePthreadMutexUnlock[rec-not-held] | &$rmtx | 0x80020001 | - |

So an initialised mutex the caller does not hold, normal or recursive, answers 0x80020001 (EPERM) on PS4, which this PR leaves as it is. Unlocking a slot that still holds the static initializer (0 or 1) was not measured, so these rows neither confirm nor contradict the new path.

@ntamas94

Copy link
Copy Markdown
Contributor

Follow-up: I have now measured the static-initializer case itself on the PS4 Pro (firmware 10.71), same homebrew probe. Each slot is a plain 8-byte variable set to 0 or 1 before the call; #info slot is its value afterwards:

pthread_mutex_unlock[static0] | (void **)&$s0 | 0x00000001 | - |
#info slot s0=0x0
pthread_mutex_unlock[static-adaptive] | (void **)&$s1 | 0x00000001 | - |
#info slot s1=0x1
scePthreadMutexUnlock[static0] | (void **)&$s2 | 0x80020001 | - |
#info slot s2=0x0
scePthreadMutexUnlock[static-adaptive] | (void **)&$s3 | 0x80020001 | - |
#info slot s3=0x1
scePthreadMutexLock[static0] | (void **)&$s2 | 0x00000000 | - |
scePthreadMutexUnlock[static0-locked] | (void **)&$s2 | 0x00000000 | - |
scePthreadMutexDestroy[static0] | (void **)&$s2 | 0x00000000 | - |
#info slot s2=0x2
scePthreadMutexUnlock[destroyed] | (void **)&$s2 | 0x80020016 | - |
scePthreadMutexLock[static-adaptive] | (void **)&$s3 | 0x00000000 | - |
scePthreadMutexUnlock[static-adaptive-locked] | (void **)&$s3 | 0x00000000 | - |
scePthreadMutexDestroy[static-adaptive] | (void **)&$s3 | 0x00000000 | - |

So the console answers exactly as this PR does: unlocking a slot that still holds 0 or 1 returns 0x80020001 (EPERM, 1 for the posix name) and leaves the slot as it was, a later lock creates the mutex, and the destroyed marker (2) returns 0x80020016.

@oneandonlydean
oneandonlydean merged commit 87f3526 into boykopovar:main Oct 23, 2026
11 checks passed
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.

5 participants