Skip to content

test(libkernel): take another port when an IPv6 socket elsewhere holds guest_tcp's listener port - #12533

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
dantdmdarealguy:test/tcp-cross-family-port-retry
Oct 29, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
dantdmdarealguy:test/tcp-cross-family-port-retry

Conversation

@dantdmdarealguy

Copy link
Copy Markdown
Contributor

What

guest_tcp aborts on the Linux CI runner in builds of pull requests that don't touch sockets:

bind of [::]:42829 failed with errno 48 while an IPv4 listener holds 0.0.0.0:42829

Cause. ListenersShareAPortAcrossFamilies binds an IPv4 listener to 0.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 with guest_tcp. If one of them holds P, [::]:P fails with EADDRINUSE (48) and the test aborts, though the guest's behaviour is correct.

Change. When the [::]:P bind fails with EADDRINUSE, the test closes its IPv4 listener and binds a fresh IPv6 socket to [::]:P:

  • Still EADDRINUSE: another socket holds P. The check runs again on a new ephemeral port, up to 64 times.
  • Bind succeeds: the test's own IPv4 listener was the conflict, so a guest IPv6 socket claimed the IPv4 port. The test aborts with the same message as before.

The check still catches the bug it was written for. With guest IPv6 sockets made dual-stack (ipv6Only = 0 in Udp.cpp's socket_nid_postfix), the changed test aborts with bind 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, the main binary against the changed one:

Load main this change
none 0/100 0/100
500 IPv6 sockets held on [::1] ephemeral ports 20/500 0/500
10000 IPv6 sockets held on [::1] ephemeral ports 75/100 0/100
six loops of guest_sce_net, guest_socket_options, guest_socket_stream_options, guest_socket_address_length running beside it for 300 s 186/2016 0/1868

Every main failure printed the CI message with errno 48.

On Windows a specific [::1]:P doesn't block a wildcard bind, and the IPv4 bind(0) skipped ports held by 5000 wildcard IPv6 sockets. So main doesn't fail there (0/100), which matches CI: guest_tcp never 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, main 7fea877. Linux: Arch Linux in WSL2 on the same machine, GCC, Release.

  • cmake --build build and cmake --build build --target libs: both succeed, with no warnings in GuestTcp.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.
  • Windows guest_tcp_tests, 100 runs each with and without 5000 held wildcard IPv6 sockets: 0 failures for both main and the change.
  • Linux stress numbers and the dual-stack check: above.
  • 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

  • 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: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes, some AI assistance was used

…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.
@dantdmdarealguy dantdmdarealguy changed the title test(libkernel): retry guest_tcp's cross-family listener check when another socket holds the port test(libkernel): take another port when an IPv6 socket elsewhere holds guest_tcp's listener port Oct 28, 2026

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

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

@adriadam10 adriadam10 self-assigned this Oct 28, 2026

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

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 jobs

Results

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

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.

@adriadam10
adriadam10 merged commit 06ed12c into boykopovar:main Oct 29, 2026
11 of 12 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the retry logic in ListenersShareAPortAcrossFamilies: EADDRINUSE with the test's IPv4 listener closed and the port still held retries on a new port (64 max), while a successful probe bind still aborts, so the dual-stack regression stays covered; errno 49 handling is unchanged. CI conventions and Windows green, Linux covered by the merge queue build; merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants