Skip to content

fix(libkernel): make close wait for a positioned transfer on the same descriptor - #5938

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
xXPBSXx:fix/libkernel-close-waits-positioned-transfer
Oct 16, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
xXPBSXx:fix/libkernel-close-waits-positioned-transfer

Conversation

@xXPBSXx

@xXPBSXx xXPBSXx commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What

Cat Quest III (PPSA15065) aborts about 15 s after start with:

what():  NativePositioned: cannot restore the file offset [fd=0 write=0 offset=12731 nbytes=1862603 saved=0 readOk=1 readError=0 restoreError=6 infoOk=0 size=0 handleFileType=0]

(PPSA15065_rN2/run_175915.err line 10; the bracketed fields come from a local debug build that extends the message, not from this change.) The read itself succeeded (readOk=1) and the restore failed with restoreError=6 (ERROR_INVALID_HANDLE), so the handle was released while the transfer was in flight.

On Windows sceKernelPread/sceKernelPwrite run as SetFilePointerEx + ReadFile/WriteFile + SetFilePointerEx back under the per-descriptor positioned lock (File::LockPositionedTransfer). sceKernelClose and close_nid_postfix did not take that lock, so a close on another thread could free the handle in the middle of a transfer. NativePositioned also fetched the handle before it took the lock, so a transfer that began after a finished close used a stale handle and failed with ESPIPE instead of EBADF.

  • sceKernelClose and close_nid_postfix take the descriptor's positioned lock before they release anything (Windows only), so a transfer in flight finishes and the close follows.
  • NativePositioned takes the lock before _get_osfhandle, so a transfer after a close sees EBADF.
  • New test guest_positioned_close (Windows): 60 rounds of a 32 MiB sceKernelPread in one thread and sceKernelClose from the main thread about 300 us later; it requires no exception, close == 0, and the read either complete or EBADF, and at least one round must overlap.

Title evidence: a trace run on the same build (PPSA15065_rN2/run_180943.err, 120 s, no crash) has 539 lines [fd0trace] close fd=0 from 7 distinct thread ids (caller app.exe+0x1401327) and 138 [fd0trace] positioned enter fd=0 lines (counted with Python); the [fd0trace] lines come from a local trace build, not from this change. So the title closes descriptor 0 repeatedly while it reads from it.

Tested

Windows 10 (19045), MinGW-w64 GCC 15.2.0, Release, on main e34c47921 + this commit:

  • guest_positioned_close fails without the fix in 4 of 4 runs (3 x what(): NativePositioned: cannot restore the file offset, 1 x result -2147352547 = ESPIPE, check failed at line 67) and passes with the fix in 5 of 5 runs.
  • The 17 libkernel file, AIO, directory and lock tests pass with the fix.
  • python tools/check_conventions.py --base upstream/main with this title and body passes.
  • Title run: not repeated with this change (the crash was seen on main a453108cf with a local debug build); the effect on the title is therefore not shown by a run with the fix.
  • Not tested: Linux (the test is Windows only), a console, other titles.

Overlaps: #5892 (Cat Quest III, open) refuses to close the standard descriptors (#4909), which removes the fd 0 reuse that produced this crash; with it the title may no longer reach this path. This change is independent of it (it fixes close against a running transfer for any descriptor) and conflicts textually with #5892 in Open.cpp (merge-tree). #3295 (open) keeps the host standard streams open on close and edits the same close functions; merge-tree conflicts only in docs/dev/TechnicalDebt.md. How the console behaves when a close races a positioned read is not verified; this change assumes the transfer completes first.

Checklist

AI-assisted (Claude Code).

🤖 Generated with Claude Code

@xXPBSXx

xXPBSXx commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Title run with this change (Cat Quest III, PPSA15065), update to the "not repeated with this change" line in the description.

Tree: upstream/main a077a88df5bc4ec26a3049aa721ac37ec8f02305 + this head abe6ba82b (merge commit 74d0c56281ecbf3e56892f1bee52d7f1cc4374b8; without #5892 and without #3295). Windows 10 (19045), MinGW-w64 GCC 15.2.0, Release, RTX 4080 SUPER; fresh libs + relinker of that tree (build 912 steps), title relinked with it, run_title.ps1 -Seconds 600, Enter/Space posted to the game window until +240 s.

Result (PPSA15065_rP1/run_060108.err, 13 lines, counted with Python): no what(), no FATAL, no NativePositioned: cannot restore the file offset, no Vulkan result -4; the app ran 06:01:30 to 06:11:30 (the 600 s limit); 20 samples: CPU 8.8 s to 925.5 s, 150 threads, working set 3.2 GB, window responding.

For comparison, without this change on main a453108cf the title aborted after about 15 s in 2 of 2 runs (run_175633.err, run_175915.err), while a 120 s trace run on the same build (run_180943.err) did not abort, so the abort is a race. One successful 600 s run with the change is therefore a hint, not a proof. Not tested: Linux, other GPUs, a second run, a picture.

AI-assisted (Claude Code).

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

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 4972 core/libs/prx/libkernel/CMakeLists.txt
PR 5021 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/File/src/Open.cpp
core/libs/prx/libkernel/File/src/Stdio.cpp
PR 5280 core/libs/prx/libkernel/CMakeLists.txt
PR 5993 core/libs/prx/libkernel/CMakeLists.txt
PR 6044 core/libs/prx/libkernel/CMakeLists.txt
PR 6222 core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6228 core/libs/prx/libkernel/File/src/Open.cpp
core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6484 core/libs/prx/libkernel/File/src/Stdio.cpp
PR 6658 core/libs/prx/libkernel/CMakeLists.txt
PR 6879 core/libs/prx/libkernel/File/src/Stdio.cpp

This was referenced Oct 15, 2026
@xXPBSXx

xXPBSXx commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@adriadam10 a short request for review of this PR. Head abe6ba82b is MERGEABLE, the Conventions, Eligibility and Intake checks are green, and the description names the title (Cat Quest III, PPSA15065), the log line and the test that fails without the change. If a review or a full Build run is possible, I will answer any finding the same day.

AI-assisted (Claude Code).

xXPBSXx and others added 2 commits October 16, 2026 20:52
… descriptor

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ansfer

Its callers (NativePositioned_nid_no_patch, the vectored transfer) already hold the
non-recursive per-descriptor lock, so locking again deadlocked every positioned transfer.

Co-Authored-By: Claude <noreply@anthropic.com>
@adriadam10

Copy link
Copy Markdown
Collaborator

Thanks for the fix. One thing added on top of your commit:

  • NativePositionedTransfer (Stdio.cpp) no longer takes LockPositionedTransfer itself. Since 65e95f7 its callers (NativePositioned_nid_no_patch and the vectored transfer) already hold that non-recursive per-descriptor mutex, so the extra lock would deadlock every positioned transfer once merged onto current main. The wrapper takes the lock before _get_osfhandle, so a transfer after a close still sees EBADF.

Checked: rebase onto current main, libkernel build and ctest on Linux (175 tests green). guest_positioned_close is Windows only, so the full Windows Build must pass before merging.

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.

2 participants