Skip to content

fix(libSceNet): retire invalidated epoll registrations - #8598

Merged
oneandonlydean merged 6 commits into
boykopovar:mainfrom
Ondra97:codex/fix-net-epoll-lifetime
Oct 22, 2026
Merged

oneandonlydean merged 6 commits into
boykopovar:mainfrom
Ondra97:codex/fix-net-epoll-lifetime

Conversation

@Ondra97

@Ondra97 Ondra97 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What

Closing a registered socket leaves its epoll entry behind. When the guest descriptor number is reused, sceNetEpollWait reports the replacement socket with the old registration's user data, and adding the replacement fails as a duplicate. A wait can also return an obsolete event mask or user data after a concurrent DEL, MOD or replacement registration.

Close now removes the descriptor from every epoll under the existing mutex. Registrations are immutable snapshots with distinct identities. After the host poll, wait checks that each registration is still current before copying its event under the same mutex. Native socket handles remain pinned through the host poll. ADD, MOD and DEL all reject closed or invalid descriptors with EBADF; DEL of an open, unregistered socket continues to return ENOENT, following Linux epoll_ctl(2). Console error ordering is unmeasured and recorded in the debt.

Eight regression cases cover reuse across two epolls, control errors for open/unregistered, closed and never-opened descriptors, close during wait, DEL, MOD, DEL followed by ADD, abort and destroy. A test-only barrier pauses after a real loopback readiness result; the test then completes the control operation before releasing the captured result. Each fixed case runs three rounds.

Descriptor identity and close cleanup follow Linux epoll(7). Console close/control ordering is unmeasured and is recorded in the library debt, together with the existing lack of duplicate-descriptor alias lifetime support. This addresses ON C1 in #8005; no game compatibility claim is made.

Tested

Windows 11 Pro 10.0.26300; AMD Ryzen 9 7900X, 32 GB RAM, NVIDIA GeForce RTX 4070 Ti SUPER. WinLibs GCC 15.2.0 UCRT r7; Release, BUILD_TESTING=ON.

Current main base: 7d7b097407a0d5d1ddd243d98f0ddc5d9cbc5420. Tested head: 05d3fe38c004d59dbe5737f29add23161b6f3f29.

The branch now includes the current main socket transport from #8653. The only manual merge conflict was in docs/dev/debt/libSceNet.md; I retained both the updated transfer debt and the epoll lifetime entry. The production epoll patch is identical to the previous head apart from line offsets and blob IDs. The explicit libSceNetCtl link added after #8268 remains in place. A new maintainer-triggered full CI run is needed for this head.

The original lifetime comparison below used base 542c65d74c95bd410f05dc70ec310d06c3fbdd36 and the original PR head c7032e6acf8c1e5c32e3be15e6b636d923cabea9; those measurements are retained from the initial validation.

Metric Before (base) After (fix)
Events from an unregistered replacement socket using the reused descriptor 1, with old user data 0
Fresh ADD of the replacement Fails, errno 48 Succeeds
Failed tests among the same 8 targeted tests 5 0

The unchanged guest_sce_net test and the abort/destroy cases pass before and after. The five failures on base are reuse, in-flight close, DEL, MOD and replacement registration. The CTest baseline was built with the unmodified base Export.cpp and the new regression.

For the review regression, the new net_epoll_lifetime_control CTest fails on the original PR head at the close-then-DEL assertion. After removing the DEL exemption, that test and the other eight focused tests pass. Each control run also checks ADD/MOD on closed and never-opened descriptors and preserves ENOENT for DEL on an open, unregistered socket. The current head passes all 43 relevant runtime CTests without skips: all eight epoll lifetime cases with three rounds each, guest_sce_net, guest_net_ctl, existing datagram and guest-buffer tests, all eleven net_dontwait_* cases from current main, both SSL transport providers, and patched-library loading. All 354 patched libraries load with zero failures.

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DPython3_EXECUTABLE=C:/Python314/python.exe -DFFMPEG_PREBUILT_DIR=D:/Github/AnyPS5/build-runtime/externals/ffmpeg-9ac4cfd195f1
cmake --build build --target relinker libs patched_libraries_tests net_epoll_lifetime_tests guest_sce_net_tests guest_net_ctl_tests guest_datagram_receive_tests guest_refused_connect_tests guest_socket_host_write_tests net_dontwait_tests net_blocking_send_tests guest_ssl_transport_tests --parallel 6
ctest --test-dir build -R '^(patched_libraries|guest_sce_net|guest_net_ctl|net_epoll_lifetime_.*|net_dontwait_.*|guest_ssl_transport_.*|guest_datagram_.*|guest_refused_connect|guest_socket_host_write_.*)$' --output-on-failure
python tools/check_conventions.py --base origin/main
python tools/pr_overlap.py --branch codex/fix-net-epoll-lifetime

#8653 is now merged, and its socket transport and updated debt are preserved. The overlap checker reports only incomplete comparisons against older conflicting PRs. I reviewed the incomplete comparisons: #5836 changes peer transport support, #6879 adds a platform include, #7201 adds platform/DNS handling, #8204 adds Ethernet text conversion, and #6044 reorganizes existing tests. None implements this fix.

No console module or game payload is needed by the tests. No game run was performed.

The full Windows runtime build passed on this head: the relinker, every system library and patched library, and all executables used by the focused test command above. This build used the submitted source without a temporary overlay.

The final main sync also includes #8543. python tools/tests/test_unwrap_self.py passes all 32 tests on this head.

The earlier all-target build at 6ac1e700578c0562bf7bcb881d18add727d374f1 required the test-only #8310 fix, exact head ca55e46f98ad85861dd7567abad0c028274daca1, for core/libs/prx/libSceAgcDriver/tests/execution/ImageArrayWindowPastLayerLimit.cpp:33. Its unchanged main version uses alignas(65536), which exceeds the supported MinGW object-file limit of 8192 (blob 0d29ae08e46e33c1cd0e931f951befcf42961ec5). #8310 remains an open dependency for that all-target build. The current validation builds the full runtime and focused test targets listed above; it does not claim a new all-target test-executable build.

python tools/check_conventions.py --base origin/main, python tools/check_test_registration.py ../build, python tools/check_main_sync.py --base origin/main --strict and git diff --check pass on this head. The registration audit reports 1146 test executables with none unregistered.

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: test(agc): align the array window test's texels at run time #8310
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@github-actions

github-actions Bot commented Oct 20, 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 9493 core/libs/prx/libSceNet/Export.cpp L1841-1842, L1852-1853, L1871-1873, L1878-1880

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 5836 core/libs/prx/libSceNet/CMakeLists.txt
core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md
PR 6044 core/libs/prx/libSceNet/CMakeLists.txt
PR 6879 core/libs/prx/libSceNet/Export.cpp
PR 7201 core/libs/prx/libSceNet/Export.cpp
PR 8225 core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md
PR 8232 core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md
PR 8234 core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md
PR 8235 core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md
PR 8254 core/libs/prx/libSceNet/CMakeLists.txt
core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md
PR 8362 core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md
PR 8658 core/libs/prx/libSceNet/Export.cpp
PR 9506 core/libs/prx/libSceNet/CMakeLists.txt
core/libs/prx/libSceNet/Export.cpp
docs/dev/debt/libSceNet.md

@nikowolf123

Copy link
Copy Markdown
Contributor

A DEL of a socket that was closed while it was still registered now fails with ENOENT. Before this PR the same call returned 0:

const int s = sceNetSocket("x", 2, 2, 17);
sceNetEpollControl(ep, 1 /* ADD */, s, &ev);
sceNetSocketClose(s);
sceNetEpollControl(ep, 3 /* DEL */, s, nullptr); // 542c65d7: 0; this PR: 0x80410102, errno 2

An ADD or MOD of the same closed id returns EBADF (errno 9), so one closed descriptor now gets two different errors. Close now drops every registration while holding g_mutex (Export.cpp#L840), so no registration can exist for an id that is not in g_socks. That means the DEL exemption at Export.cpp#L1609 never finds anything to delete, and all it does now is produce the ENOENT at L1615. Linux epoll_ctl(2), which goes with the epoll(7) model this PR follows, returns EBADF when fd "is not a valid file descriptor". shadPS4 also returns EBADF for any id that is no longer open, before it looks at registrations. How the console behaves here has not been measured.

Suggested fix: remove the exemption, and add a close-then-DEL case to NetEpollLifetime.cpp that expects a negative result with errno 9.

if (g_socks.count(id) == 0) return fail(NET_EBADF);

A DEL of an open socket that is not registered still returns ENOENT, so the existing checks in GuestSceNet.cpp#L1553-L1555 still pass. A DEL of an id that was never opened becomes EBADF, the same as ADD and MOD. I got the values above from a small probe that includes Export.cpp (WSL, g++ 15.2), run against the merge base and against this PR's head.

@Ondra97

Ondra97 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

@nikowolf123 Thanks for catching this. I removed the DEL exemption in e6ec827647, so ADD, MOD and DEL now return EBADF for closed or invalid socket descriptors. DEL of an open, unregistered socket still returns ENOENT.

I added net_epoll_lifetime_control to cover close-then-DEL, ADD/MOD/DEL on a never-opened descriptor, and the open-but-unregistered case. The regression fails at close-then-DEL on the original PR head (c7032e6acf) and passes with the fix. All 29 relevant Windows runtime tests pass without skips on the updated head (278721a708), including the existing guest_sce_net checks. Console error ordering remains unmeasured and is recorded in the library debt.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@Ondra97 This no longer links on top of current main: #8268 (merged earlier today) made libSceNet/Export.cpp call NetCtlHostDnsServers_nid_no_patch, which lives in libSceNetCtl. NetEpollLifetime.cpp includes that Export.cpp, so net_epoll_lifetime_tests now fails with an undefined reference on both Linux and Windows (CI run 39510997543).

Adding libSceNetCtl to the test's link line should fix it:

target_link_libraries(net_epoll_lifetime_tests PRIVATE libkernel libc libSceNetCtl)

Once that's in, we'll rerun full CI.

@Ondra97

Ondra97 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean Fixed in 6ac1e70057. I added libSceNetCtl to net_epoll_lifetime_tests and merged main at 6c30d8d804 into the branch.

I reproduced the undefined reference before adding the link. With it, the target builds and all 30 relevant Windows runtime CTests pass without skips, including all eight epoll lifetime cases and guest_net_ctl. The full Windows build also completes with the already-declared #8310 test-only dependency; that temporary file change was restored and is not part of this PR. Conventions and test-registration checks pass. I updated the PR description with the tested revisions and results. This is ready for the full CI rerun.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 21, 2026
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 21, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@Ondra97 thanks for adding libSceNetCtl to the test link line, and sorry for the slow reply. The epoll changes are unchanged, and the merged files compile against current main, so I'm sending it for a full CI run.

@oneandonlydean
oneandonlydean merged commit 942dec4 into boykopovar:main Oct 22, 2026
11 of 12 checks passed
@Ondra97
Ondra97 deleted the codex/fix-net-epoll-lifetime branch October 22, 2026 22:42
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