Skip to content

fix(libSceNet): register libkernel sockets with sceNetEpollControl - #11242

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
parl0v:fix/epoll-kernel-sockets
Oct 26, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
parl0v:fix/epoll-kernel-sockets

Conversation

@parl0v

@parl0v parl0v commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What

sceNetEpollControl and sceNetEpollWait now take sockets made with libkernel's socket(), not only sceNetSocket ones.

  • sceNetEpollControl accepts a descriptor that libSceNet doesn't own but libkernel has open as a socket. The libkernel check (GuestSocketIsOpen) runs outside g_mutex. Error order is unchanged: an unknown epoll id still gives EBADF, a bad op or a missing event EINVAL, a descriptor neither library has open EBADF, and a duplicate ADD EADDRINUSE.
  • sceNetEpollWait looks up the host socket of a kernel registration with KernelLookupSocketNative after g_mutex is released. That lookup asks libSceNet's own table (FindSocketNativeForKernel, which takes g_mutex) for descriptors libkernel doesn't own. The returned owner is held across WSAPoll/poll, so the host socket stays alive even if the guest closes it during the wait. The Windows closed-stream adjustment still applies only to libSceNet sockets, since it needs libSceNet's per-socket state.
  • libkernel's close() already asks libSceNet's socket closer first, and sceKernelClose now does the same. For a descriptor libSceNet doesn't own, that closer drops the descriptor's epoll registrations, the same way sceNetSocketClose does at close time. So a socket that reuses the number starts unregistered. A wait still drops a kernel registration whose socket is gone, as a fallback.
  • One new debt entry in docs/dev/debt/libSceNet.md.

Why

Human Fall Flat (PPSA02429) opens its sockets with libkernel's socket() and registers them with sceNetEpollControl. libSceNet only looked in its own table, so every ADD and DEL failed with SCE_NET_ERROR_EBADF (0x80410109). The game retries right away, from several threads, each with its own socket.

I measured it on upstream main 26ba9b6 on my test box (i5-12400F, RTX 3060, Windows): 1,674,902 failing calls in the first 30 s, half of them DEL. A temporary counter in sceNetEpollControl showed what the descriptors are. At 1.5 M calls there were 14 descriptors (17, 18, 20 to 30, 32), each in ADD/DEL pairs, and every call had the same result: the epoll id was valid, the descriptor wasn't in libSceNet's table, libkernel had it open as a socket, and KernelLookupSocketNative returned a host socket for it. There were no calls on sceNetSocket sockets.

This is the same split that MXGP 26 (PPSA30839) ran into in 8b81fa0 (sceNetSocketClose) and 3573289 (sceNetSetsockopt). I follow the position taken there: on the console both libraries work on the same kernel sockets. Epoll on libkernel sockets is not measured on a console, and the debt entry says so.

Tested

  • New CheckEpollKernelSocket in guest_sce_net, on libkernel UDP sockets:
    • ADD returns 0, and a second ADD gives EADDRINUSE.
    • A wait with timeout 0 returns 0.
    • After a sendto, the socket is reported readable, with ident and data intact.
    • MOD to writable is reported as writable.
    • DEL returns 0, a second DEL gives ENOENT, and MOD after DEL gives ENOENT.
    • After close() on one registered socket and sceKernelClose on another, waits return 0 without a crash. Then DEL and ADD on those descriptors give EBADF.
    • A descriptor that was never open still gives EBADF.
    • A socket registered and closed with sceKernelClose, whose number is then reused by a new socket: the new socket starts unregistered (ADD returns 0), and a datagram sent to it isn't reported through the old registration. Without the sceKernelClose change, guest_sce_net fails there.
  • On main with only the test applied, guest_sce_net aborts at the first ADD (0x80410109, errno 9). With the fix it passes. MinGW Release build, Windows 10.
  • Human Fall Flat for 3 minutes with the fix: 0 failing sceNetEpollControl calls, against 1,674,902 on main.
  • The game still stays on the black loading screen. That happens on main too, so this PR doesn't fix it, and I haven't found the cause yet.
  • python tools/check_conventions.py --base upstream/main passes.

Related

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 (none added)
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 3060 12 GB, NVIDIA 617.42, Windows 10 (title run); the change itself is CPU-side
  • AI-assisted: yes

Human Fall Flat (PPSA02429) opens its sockets with libkernel's socket()
and registers them with sceNetEpollControl. libSceNet only looked in its
own socket table, so every ADD and DEL failed with SCE_NET_ERROR_EBADF.
The game retries without pause: on box 2 it made more than 1.6 million
failing calls on 14 descriptors in its first seconds, all of them
libkernel sockets.

sceNetEpollControl now accepts a descriptor libkernel has open as a
socket, as sceNetSocketClose and sceNetSetsockopt already do for MXGP 26
(PPSA30839). sceNetEpollWait looks up the host socket of such a
registration through libkernel after g_mutex is released, because that
lookup asks libSceNet's own table for descriptors libkernel does not
own, and keeps the owner alive across the host poll. libkernel's close()
retires the registrations of the closed socket; a wait drops a
registration whose socket is gone.

guest_sce_net registers a libkernel UDP socket, waits for it to become
readable and then writable, deletes it twice, closes registered sockets
through close() and sceKernelClose and waits again, and checks that a
descriptor that was never open still fails with EBADF.

@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 found one correctness issue in close handling for the new kernel-socket registrations: sceKernelClose bypasses the cleanup callback, so a reused descriptor can inherit the old epoll registration before the next wait prunes it. I requested cleanup on that path and a reuse regression. Current-main conventions and diff-check pass; hosted product jobs were skipped by path policy. The author reports Windows tests, which I did not rerun locally.

std::lock_guard lk(g_mutex);
if (g_socks.count(descriptor) == 0) return false;
if (g_socks.count(descriptor) == 0) {
for (auto& epoll : g_epoll_socks) epoll.second.erase(descriptor);

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.

This cleanup only runs when KernelCloseSocket_nid_no_patch is called. sceKernelClose in libkernel/File/src/Open.cpp closes GuestSockets directly and does not invoke that callback; the registration therefore remains until a later sceNetEpollWait fails to look up the descriptor. If the descriptor number is reused for another kernel socket before that wait, KernelLookupSocketNative succeeds for the replacement and the stale epoll registration watches the new socket. Please retire these registrations on the sceKernelClose path too (or key them by stable socket identity/generation), and add a regression that closes a registered socket with sceKernelClose, immediately reuses its descriptor, and proves the old registration cannot report readiness for the replacement.

@github-actions

github-actions Bot commented Oct 25, 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 5021 core/libs/prx/libkernel/File/src/Open.cpp
PR 5836 core/libs/prx/libSceNet/Export.cpp
core/libs/tests/GuestSceNet.cpp
docs/dev/debt/libSceNet.md
PR 6936 core/libs/prx/libkernel/File/src/Open.cpp
PR 7121 core/libs/prx/libkernel/File/src/Open.cpp
PR 7201 core/libs/prx/libSceNet/Export.cpp
core/libs/prx/libkernel/File/src/Open.cpp
PR 9473 core/libs/prx/libkernel/File/src/Open.cpp
PR 9932 core/libs/prx/libSceNet/Export.cpp
core/libs/tests/GuestSceNet.cpp
docs/dev/debt/libSceNet.md
PR 9947 core/libs/prx/libkernel/File/src/Open.cpp
PR 9948 core/libs/prx/libSceNet/Export.cpp
core/libs/tests/GuestSceNet.cpp
docs/dev/debt/libSceNet.md
PR 10319 core/libs/prx/libkernel/File/src/Open.cpp
PR 11272 core/libs/prx/libSceNet/Export.cpp
core/libs/tests/GuestSceNet.cpp
docs/dev/debt/libSceNet.md
PR 11341 core/libs/prx/libSceNet/Export.cpp

@parl0v

parl0v commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

@MarcZX10 thanks, you're right. Fixed in 3cbd5a4: sceKernelClose now asks libSceNet's socket closer first, the same way libkernel's close() does, so closing a registered socket with it drops the epoll registrations right away.

The regression you asked for is in CheckEpollKernelSocket. It registers a libkernel socket, closes it with sceKernelClose, and opens sockets until one reuses that number. Then it checks that a datagram sent to the replacement isn't reported through the old registration, and that an ADD of the replacement returns 0 rather than EADDRINUSE. On my RTX 3060 box (Windows 10, MinGW Release) guest_sce_net fails with this test and without the sceKernelClose change, and passes with it. I removed the known-gap sentence from the debt entry and the body.

@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.

Re-reviewed exact head 3cbd5a4be3366ab9798c495f94809e03dc11dd05. The new sceKernelClose path calls the registered libSceNet closer; for a libkernel-owned socket, CloseSocketForKernel retires its epoll entry under g_mutex, then the existing GuestSockets::Close path closes the descriptor. The regression registers a libkernel socket, closes it with sceKernelClose, reuses the descriptor, verifies the new socket is not reported through the old registration, then registers it and confirms readiness/data. This addresses my prior stale-registration finding. Exact-head git diff --check upstream/main...HEAD and tools/check_conventions.py --base upstream/main pass. Hosted conventions, labels, eligibility and report pass; product/relinker jobs skip by path policy. The focused Windows guest_sce_net run is author-reported and I did not rerun a local product build. No remaining blocker found.

@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.

The sequential close/reuse regression covers cleanup after a completed close, but sceNetEpollControl still has a close/reuse race for libkernel-owned sockets: GuestSocketIsOpen_nid_no_patch(id) checks GuestSockets::sockets under socketsMutex, releases that lock, and only afterwards does this function reacquire g_mutex and insert the numeric descriptor into g_epoll_socks. If another thread closes id and opens a replacement in that gap, the close callback can erase no registration, then this call can register the replacement descriptor without validating its identity. A subsequent wait can therefore deliver readiness for the replacement under the old registration. Please make validation and registration retain/verify the socket lifetime (or otherwise serialize close/reuse with this insertion), and add a deterministic regression for close/reuse racing ADD. The existing sequential reuse test does not exercise this ordering. I did not run a product build locally; the focused Windows test is author-reported. Exact-head diff check and current-main conventions pass; hosted product jobs were skipped by path policy.

@oneandonlydean
oneandonlydean merged commit d9c7bb5 into boykopovar:main Oct 26, 2026
11 of 12 checks passed
AlxcBTW added a commit to anyplaynet/AnyPS5 that referenced this pull request Oct 27, 2026
…o (upstream boykopovar#11242, boykopovar#11243, boykopovar#9948) (#25)

* fix(libSceNet): register libkernel sockets with sceNetEpollControl

Human Fall Flat (PPSA02429) opens its sockets with libkernel's socket()
and registers them with sceNetEpollControl. libSceNet only looked in its
own socket table, so every ADD and DEL failed with SCE_NET_ERROR_EBADF.
The game retries without pause: on box 2 it made more than 1.6 million
failing calls on 14 descriptors in its first seconds, all of them
libkernel sockets.

sceNetEpollControl now accepts a descriptor libkernel has open as a
socket, as sceNetSocketClose and sceNetSetsockopt already do for MXGP 26
(PPSA30839). sceNetEpollWait looks up the host socket of such a
registration through libkernel after g_mutex is released, because that
lookup asks libSceNet's own table for descriptors libkernel does not
own, and keeps the owner alive across the host poll. libkernel's close()
retires the registrations of the closed socket; a wait drops a
registration whose socket is gone.

guest_sce_net registers a libkernel UDP socket, waits for it to become
readable and then writable, deletes it twice, closes registered sockets
through close() and sceKernelClose and waits again, and checks that a
descriptor that was never open still fails with EBADF.


From boykopovar#11242

* fix(libkernel): retire epoll registrations of a socket closed with sceKernelClose


From boykopovar#11242

* fix(libSceNet): read an option of a libkernel socket through sceNetGetsockopt

Human Fall Flat (PPSA02429) opens its sockets with libkernel's socket()
and reads options of them with sceNetGetsockopt. libSceNet only looked
in its own socket table, so each call failed with SCE_NET_ERROR_EBADF.
On the console both calls work on the same kernel sockets, the position
sceNetSetsockopt already takes for MXGP 26 (PPSA30839).

A descriptor libSceNet does not own but libkernel has open as a socket
now goes to libkernel's getsockopt(), which answers the options it
models. Anything else still fails with EBADF. The fallback runs after
g_mutex is released.

guest_sce_net reads SO_SNDBUF and TCP_NODELAY of a libkernel TCP socket
through sceNetGetsockopt, checks that an unknown level fails with
EINVAL, and that the call fails with EBADF once the socket is closed.


From boykopovar#11243

* fix(libSceNet): report the receive queue length in sceNetGetSockInfo

sceNetGetSockInfo always reported 0 in recv_queue_length, so a title that
reads a socket only when its record shows queued data never reads. The
record now carries the host's FIONREAD count; a socket the host refuses
FIONREAD for keeps 0, the value a PS4 Pro on firmware 10.71 reports for
an empty queue. The datagram difference between hosts and the other
measured values of the record are recorded as technical debt.


From boykopovar#9948

* fix(libSceNet): describe sockets made with socket() in sceNetGetSockInfo

The console shares one descriptor space between the kernel socket calls
and libSceNet, and sceNetGetSockInfo on a PS4 Pro (firmware 10.71)
describes and lists a socket made with socket() as it does one made with
sceNetSocket. AnyPS5 answered EBADF for such a descriptor and left it out
of the s < 0 list. libkernel now describes one of its sockets (native
handle with its owner, family, type, non-blocking mode) and lists its
descriptors; sceNetGetSockInfo reads its own table first and the kernel's
otherwise, without adopting the socket into libSceNet.


From boykopovar#9948

* test(libSceNet): drop libkernel declarations that Api.hpp already provides

---------

Co-authored-by: Josip Parlov <josipparlov@protonmail.com>
Co-authored-by: ntamas94 <ntamas@live.com>
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