Skip to content

fix(libkernel): mark a destroyed condition variable with 1 and accept destroying a null one, as measured on a PS5 - #10177

Open
orezraey wants to merge 2 commits into
boykopovar:mainfrom
orezraey:fix/libkernel-cond-destroyed-measured-ps5
Open

orezraey wants to merge 2 commits into
boykopovar:mainfrom
orezraey:fix/libkernel-cond-destroyed-measured-ps5

Conversation

@orezraey

@orezraey orezraey commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What

Destroyed condition variables measured on a retail PS5 (firmware 13.60) with homebrew probes calling libkernel_sys.sprx from a payload process. The sources and console logs are in this gist (pcond, pcond2). Split from #10130; @BunnyFl confirmed the same results against firmware 13.42 in #10130.

Destroyed condition variables. scePthreadCondDestroy stores 1 in the slot, FreeBSD libthr's THR_COND_DESTROYED. scePthreadCondWait, scePthreadCondTimedwait, scePthreadCondSignal, scePthreadCondBroadcast and scePthreadCondDestroy then all return EINVAL (0x80020016), and so does each of them for a slot that holds 1. AnyPS5 stored 2 and treated a slot holding 1 as a condition object, so it read through address 1.

Destroying an uninitialized slot (null) returns 0 and leaves it null. Before, it stored the destroyed marker. Signal, broadcast and wait on a null slot initialize it, as before.

Tested

Windows 11, MinGW (winlibs).

  • guest_pthread_destroyed_objects checks the stored 1 and a null slot through destroy, signal and broadcast. It fails on main and passes with the change.
  • 17 condition and destroyed-object tests pass three runs in a row.
  • tools/check_conventions.py passes.

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

… destroying a null one, as measured on a PS5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -175,6 +175,8 @@ int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) {
std::lock_guard lock(InitializationMutex());
if (*cond == destroyedCond())

@MarcZX10 MarcZX10 Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I rechecked the lifetime path and need to correct my concern: resolveCond copies the shared_ptr<TimedWait::Condition> while holding the same InitializationMutex, so an in-flight signal/wait retains the condition state after the private wrapper is deleted. With every slot transition serialized by that mutex, I do not have evidence that the destroy path causes the use-after-free I initially described. Please disregard my earlier lifetime claim. The implementation question I still see is narrower: the slot check uses a plain *cond read while resolveCond uses atomic_ref; both currently hold InitializationMutex, so they are serialized with each other, but the mixed access is fragile if any slot access is later moved outside the lock. I do not consider that alone a blocking issue in this patch, and no change is required based on my earlier comment.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the exact submitted head a431d14efe8d970737861fc157ef36c47a0e4524. The destroyed marker now matches the reported firmware behavior, and null destroy leaves an uninitialized slot untouched; the regression covers both cases and verifies that signal/broadcast still lazily initialize null slots. The PR reports 17 condition/destroyed-object tests passing three times on Windows, and the policy checks (eligibility, conventions, intake, labels and report) pass. Product jobs are skipped by repository path policy, and I could not independently run the focused executable because this checkout has no configured build tree. I initially raised and then retracted a condition-lifetime concern after tracing the shared ownership and initialization mutex; that concern is not a blocker.

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 9165 docs/dev/debt/libkernel.md L64-65

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 4630 docs/dev/debt/libkernel.md
PR 5021 docs/dev/debt/libkernel.md
PR 6149 docs/dev/debt/libkernel.md
PR 6740 docs/dev/debt/libkernel.md
PR 6830 docs/dev/debt/libkernel.md
PR 6936 docs/dev/debt/libkernel.md
PR 7088 docs/dev/debt/libkernel.md
PR 7090 docs/dev/debt/libkernel.md
PR 7105 docs/dev/debt/libkernel.md
PR 7121 docs/dev/debt/libkernel.md
PR 7149 docs/dev/debt/libkernel.md
PR 7509 docs/dev/debt/libkernel.md
PR 7511 docs/dev/debt/libkernel.md
PR 8010 docs/dev/debt/libkernel.md
PR 8520 docs/dev/debt/libkernel.md
PR 8553 docs/dev/debt/libkernel.md
PR 9123 docs/dev/debt/libkernel.md
PR 9160 docs/dev/debt/libkernel.md
PR 9187 docs/dev/debt/libkernel.md
PR 9199 docs/dev/debt/libkernel.md
PR 9284 docs/dev/debt/libkernel.md
PR 9293 docs/dev/debt/libkernel.md
PR 9297 docs/dev/debt/libkernel.md
PR 9302 docs/dev/debt/libkernel.md
PR 9557 docs/dev/debt/libkernel.md
PR 9561 docs/dev/debt/libkernel.md
PR 9566 docs/dev/debt/libkernel.md
PR 9575 docs/dev/debt/libkernel.md
PR 9693 docs/dev/debt/libkernel.md
PR 9947 docs/dev/debt/libkernel.md
PR 10319 docs/dev/debt/libkernel.md

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I checked the overlap warning against #9165. That PR adds a separate timed self-lock debt bullet; this PR updates the condition-wait entry and changes only Cond.cpp plus the destroyed-object regression. The implementation behavior is independent. If stale debt-file context causes a rebase conflict, preserve both entries; no Depends on relationship is needed.

@oneandonlydean oneandonlydean self-assigned this Oct 23, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 23, 2026

This branch has not been deployed

No deployments
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.

3 participants