Repository navigation
test(libkernel): take another port when an IPv6 socket elsewhere holds guest_tcp's listener port - #12533
Conversation
…s guest_tcp's listener port ListenersShareAPortAcrossFamilies binds an IPv4 listener to an ephemeral port and then binds [::] to the same port, expecting the IPv6-only socket to succeed. On Linux the IPv4 ephemeral choice ignores IPv6-only sockets, so another process's IPv6 socket on that port makes the [::] bind fail with EADDRINUSE and the test aborts. Linux CI jobs 122918896857 and 122033051054 failed this way with pull requests that touch no sockets. When the [::] bind fails with EADDRINUSE, the test now closes its IPv4 listener and binds a fresh IPv6 socket to the port. If that bind still fails with EADDRINUSE, another socket holds the port and the check runs again on a new ephemeral port, up to 64 times. If it succeeds, the IPv4 listener was the conflict and the test aborts as before.
rooklz
left a comment
There was a problem hiding this comment.
Independent host-side check of @babbf88deb (no console involved). I can't build the libs lane on macOS, so this checks the socket primitives with a native C harness. It copies the old and new ListenersShareAPortAcrossFamilies flow line for line (guest IPv6 = IPV6_V6ONLY=1, as in Udp.cpp:1241). Holders are IPV6_V6ONLY TCP sockets bound to [::1]:0 and kept open, the same load as in the body.
Linux (Ubuntu 24.04 amd64 container, port range 32768-60999), 200 runs per row:
| holders | old test fails | new test fails | max attempts |
|---|---|---|---|
| 0 | 0 | 0 | 1 |
| 2000 | 34 | 0 | 4 |
| 10000 | 151 | 0 | 19 |
| 12000 | 77/100 | 0/100 | 19 |
| 14000 | 99/100 | 61/100 | 64 (exhausted) |
That fits your WSL2 numbers. The collision rate is this high because Linux bind(0) hands out only odd ports (I probed 2000/2000 odd). The holders and the IPv4 listener therefore draw from the same ~14100-port half, so 64 attempts run out only once about half of that pool is held. Real suite load is nowhere near that.
Dual-stack control (V6ONLY=0, Linux): the old and new tests both fail 50/50 with errno 98 (EADDRINUSE). The probe bind succeeds once the IPv4 listener is closed, so the regression aborts with the original message. I tried two mutants: HeldByAnotherInet6Socket always returning true, and probing before closing the IPv4 listener. Both still fail 20/20, but only after 64 attempts with the generic message. The close-then-probe order is therefore what keeps the specific diagnostic, and it is correct as written.
macOS 26.2 arm64 (non-blocking, already there before this PR): 0 failures for old and new at 0/2000/10000 holders. More importantly, a dual-stack [::]:P bind succeeds next to a 0.0.0.0:P listener (v6only=0 bind=0 listen=0). So on macOS this test can't detect the dual-stack regression on either revision. Not this PR's job. It might be worth a line if the test ever gets registered for macOS.
check_conventions --base upstream/main: passed. merge-tree against main f21ef7c: clean. The diff is test-only and I see no issues. I'm leaving a COMMENT rather than an approval only because I couldn't compile GuestTcp.cpp itself here.
AI-assisted: yes
MarcZX10
left a comment
There was a problem hiding this comment.
Reviewed — looks good
The bounded retry checks whether an EADDRINUSE port remains occupied after releasing the test’s IPv4 listener, and the available Linux stress evidence supports the change without weakening the dual-stack regression assertion.
What I ran
Reviewed head babbf88debfbefdcfb663c3644b37b4669984ce3 against base e4da19aff06e25ae16b90f460dff22e8ac1eb786; it merges cleanly onto current main. Local review environment: Debian 12 x86_64. I ran merge-tree and whitespace checks; I did not compile GuestTcp.cpp or run guest_tcp locally.
git merge-tree $(git merge-base upstream/main pr/12533) upstream/main pr/12533
git diff --check upstream/main...pr/12533
gh pr checks 12533 -R boykopovar/AnyPS5
gh run view 40741014802 -R boykopovar/AnyPS5 --json jobsResults
| Check | Result |
|---|---|
Merge onto current main |
Clean |
git diff --check |
Pass; no output |
| Hosted Windows job | Pass; configured build/test selection and CTest steps were skipped, but host-only test linking ran |
| Conventions, eligibility, intake, labels | Pass |
| Hosted Linux and specialized jobs | Skipped |
| Focused runtime and stress checks | Reported by the PR author; not independently rerun by me |
| Independent socket behavior check | Reported by reviewer rooklz; native Linux harness and macOS socket probes, not a build of GuestTcp.cpp |
Command output
$ git merge-tree $(git merge-base upstream/main pr/12533) upstream/main pr/12533 | head -3
merged
result 100644 91c3bf185c9aca6c7dc18b9ff6262785202e27de core/libs/tests/GuestTcp.cpp
our 100644 f8677b9e79a40d7503be5c576fc3cf795b794540 core/libs/tests/GuestTcp.cpp
$ git diff --check upstream/main...pr/12533
(no output)
$ gh pr checks 12533 -R boykopovar/AnyPS5
labels pass
windows pass
linux skipping
register-context skipping
relinker skipping
relinker-asan skipping
write-watch skipping
conventions pass
eligibility pass
intake pass
Windows job steps:
- Check that every test executable is registered with ctest: success
- Link changed host-only core/libs test executables: success
- Select tests, build selected tests, and run CTest: skipped
- Build libs: skipped
Negative control
| Assertion | Change under test | Did the regression test fail? |
|---|---|---|
| Dual-stack IPv6 must still conflict with the IPv4 listener | Not run by me. The PR author reports a dual-stack mutant abort; reviewer rooklz reports the old and new probe both fail under IPV6_V6ONLY=0 |
Author and reviewer report yes; not independently reproduced by me |
Findings
Blocking
None found in the changed test logic.
Non-blocking
The Linux runtime/stress results are reported by the author and the independent reviewer; hosted Linux CTest was skipped in this run. I did not reproduce those runs locally.
Sources
- Changed
GuestTcpregression test at the reviewed head - IPv6 sockets are configured IPv6-only at the reviewed head
- PR test and stress report
- Independent socket behavior review
Not verified
I did not compile or run the test binary locally, run the stress loops myself, test on a PS5, or run the skipped hosted Linux and specialized jobs.
|
Checked the retry logic in |
What
guest_tcpaborts on the Linux CI runner in builds of pull requests that don't touch sockets:main17f8dfa + build(libs): support Emscripten memory64 compilation #11341 (Emscripten build support).Cause.
ListenersShareAPortAcrossFamiliesbinds an IPv4 listener to0.0.0.0:0, reads back the port P the host picked, and then binds a new guest IPv6 socket to[::]:P. Guest IPv6 sockets are IPv6-only (#4370), so that bind must succeed.The host picks P only from IPv4's point of view. On Linux, an IPv6-only socket on P doesn't stop an IPv4 wildcard bind, so the kernel may hand out a port that some other IPv6 socket already holds. Many suite tests (
guest_sce_net,guest_socket_*) create IPv6 sockets, and ctest runs them in parallel withguest_tcp. If one of them holds P,[::]:Pfails with EADDRINUSE (48) and the test aborts, though the guest's behaviour is correct.Change. When the
[::]:Pbind fails with EADDRINUSE, the test closes its IPv4 listener and binds a fresh IPv6 socket to[::]:P:The check still catches the bug it was written for. With guest IPv6 sockets made dual-stack (
ipv6Only = 0inUdp.cpp'ssocket_nid_postfix), the changed test aborts withbind of [::]:59631 failed with errno 48 while an IPv4 listener holds 0.0.0.0:59631.Measurements. Arch Linux in WSL2 on the machine below, Release
guest_tcp_tests, themainbinary against the changed one:main[::1]ephemeral ports[::1]ephemeral portsguest_sce_net,guest_socket_options,guest_socket_stream_options,guest_socket_address_lengthrunning beside it for 300 sEvery
mainfailure printed the CI message with errno 48.On Windows a specific
[::1]:Pdoesn't block a wildcard bind, and the IPv4bind(0)skipped ports held by 5000 wildcard IPv6 sockets. Somaindoesn't fail there (0/100), which matches CI:guest_tcpnever failed on Windows.Tested
Windows 11 Pro, Intel Core i9-14900K, 128 GB RAM, NVIDIA GeForce RTX 5080 (driver 32.0.16.1742), WinLibs GCC 15.2.0, Release,
BUILD_TESTING=ON,main7fea877. Linux: Arch Linux in WSL2 on the same machine, GCC, Release.cmake --build buildandcmake --build build --target libs: both succeed, with no warnings inGuestTcp.cpp.ctest -R "^guest_tcp$|patched_libraries|^guest_udp$|^guest_socket_|^guest_sce_net$": 32/32 passed.patched_libraries:368 patched libraries loaded, 0 failed.guest_tcp_tests, 100 runs each with and without 5000 held wildcard IPv6 sockets: 0 failures for bothmainand the change.python tools/check_conventions.py --base origin/main: passed.python tools/check_test_registration.py build:1513 test executable(s), 0 built and not run as recorded, none new.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)