Skip to content

fix(libkernel): keep host standard streams open on guest close - #3295

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
SynnZer0:fix/libkernel-close-host-streams
Oct 14, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
SynnZer0:fix/libkernel-close-host-streams

Conversation

@SynnZer0

Copy link
Copy Markdown
Contributor

What

close and sceKernelClose no longer close descriptors 0 to 2 that were open when libkernel loaded. They fail with EBADF and leave the descriptor open.

Guest descriptors are host C runtime descriptors. When a title closed descriptor 0, the host process's stdin was closed, and the C runtime then gave descriptor 0 to the next file opened anywhere in the process, including files libkernel opens itself, such as the std::ifstream of the APR reader (Apr.cpp _readFile). The title's next close of descriptor 0 then closed that file during the read.

PPSA21703 (Vampire Survivors, Unity) calls sceKernelClose(0) repeatedly while loading its asset bundles: 27 times in one run, every call after the first failing with EBADF (temporary logging, not part of this PR). The symptoms were different on every run:

  • APR: read of …\vs_local_assets_character_<name>.bundle at offset 0 returned 0 of N bytes, and partial reads such as 13833 of 15745;
  • APR: read failed for …;
  • FATAL: UCRT invalid parameter from close_nid_postfix, which, unlike sceKernelClose, calls _close without suppressing the invalid-parameter handler.

When a read failed, the destination memory was committed PAGE_READWRITE and the files were complete on disk, so this is not write-watch protection or the APR execution order. APR itself is unchanged.

  • File/src/HostStreams.cpp (new): File::IsHostStream(fd) records at load whether 0 to 2 are open (_get_osfhandle with the invalid-parameter handler suppressed on Windows, fcntl(F_GETFD) elsewhere). A standard descriptor that was not open at load (no console) can still be returned by open and closed normally.
  • sceKernelClose → SCE_KERNEL_ERROR_EBADF; close / _close → -1, errno EBADF, for those descriptors.
  • docs/dev/TechnicalDebt.md: the console's result for closing 0 to 2 is unknown; recorded with the title that needs it.
  • tests/GuestHostStreams.cpp (new, guest_host_streams): for each of 0 to 2 that is open, both calls fail with EBADF and the descriptor stays usable; a new pipe does not reuse those numbers.

Tested

Windows 11, MinGW-w64 GCC 15.2.0 (WinLibs 15.2.0posix-14.0.0-ucrt-r7), full build with BUILD_TESTING=ON:

  • ctest: 768/769 passed. The failure is guest_filesystem (CreateSymbolicLinkW without Developer Mode), which also fails on the base on this machine.
  • python tools/check_conventions.py --base origin/main: passed.
  • PPSA21703: before, the APR read failure ended the title within ~10 s on every run. After, three 90 s runs had no APR or UCRT errors, and the title reaches its first screen at 60 FPS. Reaching that screen also needs the module-loading work in feat(relinker): add --module-dir for modules loaded at run time #846 and the two related PRs below; those are separate topics and not needed for this fix.
  • Linux and macOS: not built or run (HostStreams.cpp uses fcntl there).

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

@github-actions github-actions Bot added the intake paused Closed while PR intake is limited to authors with at least 10 PRs merged into main label Oct 11, 2026
@github-actions

Copy link
Copy Markdown

Thank you for your interest in the project! We are sorry, but we cannot accept more PRs while we work through the existing backlog. This PR has been automatically closed and labeled "intake paused". This is not a judgment of the quality of your changes.

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

Copy link
Copy Markdown
Collaborator

Needs a rebase: it conflicts with main in core/libs/prx/libkernel/CMakeLists.txt, File/src/Open.cpp and File/src/Stdio.cpp, and docs/dev/TechnicalDebt.md has moved to docs/dev/debt/<library>.md (tools/debt_split.py resolve moves the entries).

@Nesyth

Nesyth commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Data point from Vampire Survivors (PPSA21703) on today's main (9d0a41d), Windows 11, RX 7900 XTX:

  • Without this change, 2 of 2 launches abort about 15 s in, while the title loads its asset bundles:
    • APR: read failed for ...\vs_local_assets_character_gennaro.bundle, after two FATAL: UCRT invalid parameter reports
    • APR: read of ...\vs_local_assets_character_imelda.bundle at offset 0 returned 12023 of 18326 bytes
  • In the first of those runs the title's save write also failed: Error writing save data: Invalid handle to path "/savedata0/SaveData". The title opens that file with truncate, so SaveData was left at 0 bytes (21,557 before).
  • With the same main plus a simpler guard for the same bug (close and sceKernelClose refuse descriptors 0 to 2), the title loads and plays, and the save stays intact.

Guest descriptors are host C runtime descriptors. Closing descriptor 0, 1
or 2 closed the host process's standard stream, and the runtime then gave
that number to the next file opened anywhere in the process, including
files libkernel opens itself, such as the APR reader's. The title's next
close of the same number closed that file mid-read.

close and sceKernelClose now fail with EBADF for descriptors 0 to 2 that
were open when libkernel loaded, and leave them open. A standard
descriptor that was not open at load can still be opened and closed by
the guest.

PPSA21703 calls sceKernelClose(0) repeatedly while loading asset
bundles, which made APR reads return 0 or a part of the requested bytes
and raised UCRT invalid parameter reports from close.

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

Copy link
Copy Markdown
Contributor

@SynnZer0 @oneandonlydean The rebase asked for above is ready on nikowolf123/AnyPS5 help/S1-host-streams. It is one commit on main a453108, 802942849, with SynnZer0 still the author and the message unchanged, and it merges cleanly with today's main (e34c479). This PR allows maintainer edits, so the branch can replace its head as it is:

git fetch https://github.com/nikowolf123/AnyPS5 help/S1-host-streams
git push --force-with-lease=fix/libkernel-close-host-streams:9c30f0e4a527b5d3703e85aea37012b2c8b5a273 https://github.com/SynnZer0/AnyPS5 FETCH_HEAD:fix/libkernel-close-host-streams

Only the conflicting lines changed; git range-diff against 9c30f0e shows nothing else:

  • In sceKernelClose and close, the File::IsHostStream(d) check is still the first line, and main's socket and EqueueDescriptorClosed lines follow unchanged. The old d >= GuestSockets::FirstDescriptor check from this PR's base is gone on main.
  • In CMakeLists.txt, guest_host_streams is registered after main's new guest_kqueue and guest_socket_descriptors.
  • tools/debt_split.py resolve moved the debt entry to docs/dev/debt/libkernel.md.

Tested:

  • WSL Ubuntu, GCC 15.2, Release: on main a453108 with only the test added, guest_host_streams aborts. On the branch it passes, as do all 137 tests registered in core/libs/prx/libkernel, and tools/check_test_registration.py passes.
  • Windows 11 Pro, MinGW-w64 GCC 15.2.0 (WinLibs r7): on main with only the test added, guest_host_streams aborts (0xc0000409). On the branch it passes. guest_dup, guest_filesystem, guest_terminal, guest_kqueue, guest_socket_descriptors and guest_socket_pair pass on both.
  • main's tools/check_conventions.py --base origin/main passes for the branch with this PR's current title and description.

Besides Blasphemous 2 (#4223) and Vampire Survivors (@Nesyth's runs above), there is now a third title: @WallisSkill reports in #5892 that Cat Quest III (PPSA15065) stays on the loading screen (#4909) without that PR's commit 1, 4485d02f6. That commit is a narrower copy of this fix: it changes sceKernelClose only, returns EBADF for descriptors 0 to 2 whether or not the host has them open, and adds no test or debt entry. Once this PR lands, #5892 can drop it.

#5892 and @xXPBSXx's #5938 (opened today) also edit sceKernelClose, and #5938 edits close too. git merge-tree merges the rebased branch cleanly with each of them.

AI-assisted: written with Claude Code; I checked the conflict resolution with git range-diff, the merges with main e34c479, #5892 and #5938 with git merge-tree, and the title evidence in #4223, #5892 and the comment linked above. I ran the WSL and Windows builds and tests listed above, check_test_registration.py and check_conventions.py.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Pushed @nikowolf123's rebase (8029428) here; it merges cleanly with current main.

@oneandonlydean
oneandonlydean force-pushed the fix/libkernel-close-host-streams branch from 9c30f0e to 8029428 Compare October 14, 2026 17:45
@github-actions

github-actions Bot commented Oct 14, 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
#5835 core/libs/prx/libkernel/File/src/Open.cpp L13
core/libs/prx/libkernel/File/src/Stdio.cpp L18

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
@oneandonlydean
oneandonlydean merged commit bebff8b into boykopovar:main Oct 14, 2026
8 checks passed
adriadam10 pushed a commit that referenced this pull request Oct 15, 2026
The area-labels workflow listed open PRs with -L 200, so with more than 200
open PRs the older ones never entered the todo list and never got their area
labels (#3295 changes libkernel and has no libkernel label). List up to 1000;
the head -n 40 cap still bounds labeler work per run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

intake paused Closed while PR intake is limited to authors with at least 10 PRs merged into main

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants