Repository navigation
Track implicit autobind ports from listen() in Consomme networking - #41125
Merged
Ben Hillis (benhillis) merged 6 commits intoJul 24, 2026
Merged
Conversation
WSL2's Consomme networking mode only forwards TCP/UDP ports that were explicitly bind()'d in the guest. If an application calls listen() without first calling bind() (relying on the kernel's implicit autobind to an ephemeral port), the port tracker never sees it, so the port is never forwarded to the host and the socket is unreachable via 127.0.0.1:from Windows. The seccomp filter that traps socket syscalls for port tracking only ever trapped bind(); this extends it to also trap listen() across all supported architectures (x86_64, x86 compat via socketcall, and ARM/ARM64), and dispatches it through the existing DeferredPortLookup/ResolvePortZeroBind mechanism already used for explicit bind(port=0). To avoid adding latency to the overwhelmingly common bind()+listen() sequence (where the port is already known), ParseListen first checks getsockname() synchronously and only falls back to the deferred resolution path for genuine implicit-autobind-via-listen() calls (no prior bind()). Adds ListenWithoutBindIsTracked test coverage in both Consomme and Mirrored NetworkTests suites. Fixes #41117 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a Consomme-mode port-forwarding gap where TCP/UDP listeners created via listen() without a preceding explicit bind() (kernel implicit autobind to an ephemeral port) were not observed by the guest port tracker, leaving the socket unreachable from Windows via 127.0.0.1:.
Changes:
- Extend the seccomp BPF filter and dispatcher registration to intercept
listen()in addition tobind()(including x86 compatsocketcall(SYS_LISTEN, ...)and 32-bit ARM compat viaARMV7_NR_listen). - Teach
GnsPortTracker::GetCallInfo()to recognizelisten()notifications and resolve the effective port viagetsockname()(fast-path when already bound) or the existing deferredResolvePortZeroBind()mechanism (implicit autobind case). - Add new NetworkTests coverage for the
listen()-without-bind()scenario in both Consomme and Mirrored suites.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/NetworkTests.cpp | Adds ListenWithoutBindIsTracked test coverage and a guest helper that creates a listener via listen() without bind(). |
| src/linux/init/main.cpp | Updates seccomp BPF filter to trap listen() alongside bind()/ioctl(...) across supported ABIs. |
| src/linux/init/localhost.cpp | Registers a seccomp handler for __NR_listen and for ARMV7_NR_listen in compat mode. |
| src/linux/init/GnsPortTracker.cpp | Adds parsing/dispatch for listen() notifications and reuses deferred port resolution for implicit autobinds. |
| src/linux/inc/seccomp_defs.h | Introduces ARMV7_NR_listen constant for 32-bit ARM compat syscall trapping. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b
- Update RegisterSeccompHook doc comment to reference SIOCSIFFLAGS (matching the actual BPF filter) instead of the incorrect TUNSETIFF. - Update ParseListen's comment to reflect the getsockname() fast-path added before deferring resolution, instead of describing it as always deferring. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b
ioctl(*, SIOCSIFFLAGS, *) is only trapped in the native 64-bit BPF block, not the 32-bit compat blocks (socketcall/ARMV7), which only trap bind()/listen(). Clarify the routine description accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30cabf31-6198-44c8-b078-2a2ce1c6c56b
Ben Hillis (benhillis)
marked this pull request as ready for review
July 21, 2026 21:08
Only parse the PORT= value once the line is terminated by a newline, instead of stopping at the first non-digit character seen in a partial ReadFile() buffer. This avoids truncating the port number (e.g. reading "123" as "12") when the perl helper's output is split across multiple reads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 85780ea9-1bf4-4997-8732-b33920e7d6b8
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/linux/init/main.cpp:3368
- The comment above the
__AUDIT_ARCH_64BITcheck is misleading: the filter does not “notify on all non-native arch”; it jumps to the compat block and applies a narrower match there. Updating the comment will help future edits avoid incorrect assumptions about the control flow.
// For now, notify on all non-native arch
BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __AUDIT_ARCH_64BIT, 0, 8),
Comment on lines
+465
to
+475
|
auto ParseListen = [&](int Socket) -> std::optional |
||
| auto networkNamespace = std::filesystem::read_symlink(std::format("/proc/{}/ns/net", Pid)).string(); | ||
| if (networkNamespace != m_networkNamespace) | ||
| { | ||
| GNS_LOG_INFO("Skipping listen() call for pid {} in network namespace {}", Pid, networkNamespace.c_str()); | ||
| return {{{}, {}, CallId}}; // Different network namespace. Let it go through. | ||
| } | ||
|
|
||
| try | ||
| { | ||
| const int protocol = GetSocketProtocol(Pid, Socket); |
If the target process exits between the seccomp trap and this call, read_symlink() could throw outside of the try block, routing through the outer GetCallInfo catch/log path with a bind()-focused error message instead of the intended 'not an IP socket, let it through' handling used by the rest of ParseListen. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 10ce6d20-f8ca-4204-92a2-48e23bb6ee66
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/linux/init/main.cpp:3368
- This BPF comment is misleading: the code does not "notify on all non-native arch"; it jumps to the compat blocks below which then selectively notify on bind/listen. Clarify the intent so future edits don’t misinterpret the control flow.
// For now, notify on all non-native arch
BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, __AUDIT_ARCH_64BIT, 0, 8),
| L"listen(S,5) or die;" | ||
| L"my $port=(sockaddr_in(getsockname(S)))[0];" | ||
| L"print \"PORT=$port\\n\";" | ||
| L"accept(C,S);" |
Blue (OneBlue)
approved these changes
Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
WSL2's Consomme networking mode only forwards TCP/UDP ports that were explicitly
bind()'d in the guest. If an application callslisten()without first callingbind()(relying on the kernel's implicit autobind to an ephemeral port on the wildcard address), the port tracker never observes the bind, so the port is never registered for host forwarding and the socket is unreachable via127.0.0.1:from Windows.Fixes #41117
Root cause
The seccomp filter installed by
RegisterSeccompHook()insrc/linux/init/main.cpponly ever trappedbind().listen()was never trapped, so asocket()+listen()sequence with no priorbind()call was invisible toGnsPortTracker.What this changes
listen()across all supported architectures (native 64-bit__NR_listen, x86 compat viasocketcall(SYS_LISTEN, ...), and 32-bit ARM EABI via a newARMV7_NR_listenconstant).GnsPortTracker::GetCallInfodispatcheslisten()calls through a newParseListenpath, reusing the existingDeferredPortLookup/ResolvePortZeroBindmechanism already used for explicitbind(port=0).bind()+listen()sequence (where the port is already known beforelisten()runs),ParseListenfirst checksgetsockname()synchronously and only falls back to the deferred/polling resolution path for genuine implicit-autobind-via-listen()calls (no priorbind()).Testing
ListenWithoutBindIsTrackedto bothNetworkTests::ConsommeTestsandNetworkTests::MirroredTests, mirroring the existingPortZeroBindIsTrackedcoverage but exercisinglisten()with no precedingbind().ConsommeTests::ListenWithoutBindIsTrackedfails (host can bind the port — Consomme never tracked it).MirroredTests::ListenWithoutBindIsTrackedpasses both before and after — mirrored mode already has an independent port+protocol refresh mechanism (OnRefreshAllocatedPorts, added in Fix mirrored mode port tracking for implicit binds resulting from accept() calls #40287) that covers this case, so this change doesn't affect mirrored mode's behavior.listen()and the new filter does (__NR_listen, syscall nr 50 on x86_64).