Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[has a simply repro] TCP ephemeral ports exhausted after lots of early-closed non-blocking connections #3951

Open
YihaoPeng opened this issue Apr 1, 2019 · 5 comments

Comments

Projects
None yet
5 participants
@YihaoPeng
Copy link

commented Apr 1, 2019

This issue is a repetition of #2913, just to remind Microsoft that #2913 already has a very simple code to reproduce.

In the three months after the reproduction code proposed by philip-searle on 18 Jan, no Microsoft employees paid attention to the old issue. So I can only make a new issue to get attention.

Description

After creating and closing (before established) a large number of non-blocking connections in WSL, all TCP ephemeral ports will be exhausted, then no new TCP connections from WSL or Win32 can be established. Closing related processes in WSL does not release these ports. All new TCP connections or listening will failed and must to restart the LxssManager service to recover.

Reproducible Demo

The demo is from philip-searle's comment of #2913 on 18 Jan.

You can reliably reproduce this issue using the attached program (~80 lines of C): wsl-issue-2913-repro.c.txt

Output from strace looks normal to me and is attached as wsl-issue-2913-repro.strace.zip

The program performs these steps in a loop:

  1. Allocate a TCP socket for IPv4 (AF_INET) family.
  2. Make the socket non-blocking with O_NONBLOCK.
  3. Attempt to connect() the socket to 127.0.0.1:1234
  4. Use getsockname() to obtain the port number and output it to the console.
  5. Close the socket

Environment

In Ubuntu in WSL, build and run the demo with these commands:

apt update
apt install gcc wget
wget -O wsl-issue-2913-repro.c https://github.com/Microsoft/WSL/files/2769821/wsl-issue-2913-repro.c.txt
gcc -o wsl-issue-2913-repro wsl-issue-2913-repro.c
./wsl-issue-2913-repro

Expected Behavior

On a Linux VM I can run the loop several hundred thousand times and see the ports being used cycle through the entire ephemeral range multiple times.

In addition, even if the program has a bug that does not properly release the occupied port, these ports should be automatically released after the program exits.

Observed Behavior

On philip-searle's Windows laptop it loops about 16,000 times and then EINVAL is returned from connect(). At this point the symptoms described in previous comments appear: Win32 programs such as web browsers fail to connect and the output of "netstat -anoq" in a command prompt shows many connections stuck in the "BOUND" state. The only way to get network connections working again is to restart the LxssManager service.

On YihaoPeng's PC with Windows 1809 build 17763.379, the ports will be exhausted after 2899 rounds:

...
Socket 2893 is using port 4930
Socket 2894 is using port 4943
Socket 2895 is using port 4944
Socket 2896 is using port 4945
Socket 2897 is using port 4952
Socket 2898 is using port 4953
Invalid argument returned from connect() - BOUND socket exhaustion likely after 2899 rounds
Exiting (failed)

No ports released after the program exits. If you let the program run repeatedly (so it will immediately take up the ephemeral port released by other programs), you will find that no TCP connections in your Windows can be established. For example, your EDGE browser will not be able to load any page.

Use the following commands to run the program repeatedly:

while true; do ./wsl-issue-2913-repro; done

@YihaoPeng YihaoPeng changed the title [has a simply repro] TCP user port exhaust and network broken after lots of non-blocking connections [has a simply repro] TCP user ports exhausted after lots of early-closing non-blocking connections Apr 1, 2019

@YihaoPeng YihaoPeng changed the title [has a simply repro] TCP user ports exhausted after lots of early-closing non-blocking connections [has a simply repro] TCP ephemeral ports exhausted after lots of early-closing non-blocking connections Apr 1, 2019

@therealkenc

This comment has been minimized.

Copy link
Collaborator

commented Apr 1, 2019

Phillip's code below so it doesn't get lost.

#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

const char *const HOSTNAME = "127.0.0.1";
const char *const PORT = "1234";

void die(const char *const format, ...)
{
	va_list argList;
	va_start(argList, format);
	vprintf(format, argList);
	va_end(argList);
	puts("Exiting (failed)");
	exit(EXIT_FAILURE);
}

static void make_socket_non_blocking(int socket_fd)
{
	int flags = fcntl(socket_fd, F_GETFL, 0);
	if (flags == -1) {
		die("Failed to get socket flags: %d", socket_fd);
	}

	flags |= O_NONBLOCK;
	if (fcntl(socket_fd, F_SETFL, flags) == -1) {
		die("Failed to set socket flags: %d", socket_fd);
	}
}

int main(int argc, char **argv)
{
	struct addrinfo hints = { 0 };
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;

	struct addrinfo *res = 0;
	int err = getaddrinfo(HOSTNAME, PORT, &hints, &res);
	if (err != 0) {
		die("failed to resolve socket address (err=%d)\n", err);
	}

	int socket_fd = -1;

	unsigned int sockets_allocated = 0;
	for (;;) {
		socket_fd =
		    socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (socket_fd == -1) {
			die("Failed to allocate socket after %d rounds: %s\n",
			    sockets_allocated, strerror(errno));
		}
		make_socket_non_blocking(socket_fd);

		if (connect(socket_fd, res->ai_addr, res->ai_addrlen) == -1) {
			if (errno == 22) {
				die("Invalid argument returned from connect() - BOUND socket exhaustion likely after %d rounds\n", sockets_allocated);
			}
			/* We expect not to be able to connect since there is nothing listening on our target port. */
			/* printf("Failed to connect socket, this is expected (%d): %s\n", errno, strerror(errno)); */
		}

		struct sockaddr_in addr = { 0 };
		socklen_t addrlen = sizeof addr;
		if (getsockname(socket_fd, &addr, &addrlen)) {
			printf
			    ("Failed to get name of socket after %d rounds: %s\n",
			     sockets_allocated, strerror(errno));
		} else {
			/* sockaddr_in is big-endian (except address family) */
			addr.sin_port = ntohs(addr.sin_port);
			printf("Socket %d is using port %d\n",
			       sockets_allocated, addr.sin_port);
		}

		close(socket_fd);
		sockets_allocated++;
	}

	freeaddrinfo(res);
}

@YihaoPeng YihaoPeng changed the title [has a simply repro] TCP ephemeral ports exhausted after lots of early-closing non-blocking connections [has a simply repro] TCP ephemeral ports exhausted after lots of early-closed non-blocking connections Apr 2, 2019

@mpa7

This comment has been minimized.

Copy link

commented Apr 2, 2019

Running the Sysinternals RamMap utility after the above program has terminated provides some insight - the wsl-issue-2913-repro process still exists as a zombie - no surprise considering it owns all those sockets visible in netstat.exe -anoq:

image

There's actually no need to go to the extreme of port exhaustion to repro the underlying issue. In fact we don't need the above code at all, we can use anything that will call connect() and can be fed a target where there is nothing listening.

Zombie wget and curl (sticking with 127.0.0.1:1234):

wget http://127.0.0.1:1234
curl http://127.0.0.1:1234

Can also try for the same result (it's not a localhost thing):

curl http://www.microsoft.com:1234

If there is something listening on 127.0.0.1:1234 (I used netcat) there is no zombie process:

sudo apt install netcat-openbsd
nc -l 127.0.0.1 1234
curl http://127.0.0.1:1234 (need to now ^D in netcat)

So it seems that WSL does not correctly handle failed connects. It looks as though a reference to the socket is maintained internally which results in the owning process becoming a zombie and port exhaustion will eventually (or quickly) occur as per #2913.

As mentioned in #2913 terminating all WSL processes running under the distro allows the sockets to be released and the zombies go away once WSL runs its cleanup.

@therealkenc

This comment has been minimized.

Copy link
Collaborator

commented Apr 2, 2019

There's actually no need to go to the extreme of port exhaustion to repro the underlying issue. In fact we don't need the above code at all, we can use anything that will call connect() and can be fed a target where there is nothing listening.

Great follow up.

I was able to reproduce this here on 18865 with RamMap and your wget repro.

This is going to 'splain some other ill-defined reports, if WSL has been leaking like a sieve all this time nd no one noticed. Usually you don't try to connect to something that isn't there, and those NT processes don't show up in a bog-standard Resource Monitor look see. But if, say, you did an apt update and dozens (or hundreds) of fetches failed for whatever reason, that would make a mess.

@roy-bentley

This comment has been minimized.

Copy link

commented Apr 22, 2019

I am also experiencing this issue -- it's very frustrating to have to bounce LXSSManager a couple times a day.

It appears that some process (pstree shows it being spawned by init, which isn't actually the case but I'm not clever enough to determine who's firing it) is spawning timeout 1 wget -q -O- --tries=1 http://169.254.169.254 on a really tight loop. Somehow, the port bound to this process isn't released after it runs and can exhaust 55535 ports in less than a day.

There's probably better ways to get some of this data...:

Ephemeral port usage

C:\>netsh int ipv4 show dynamicport tcp

Protocol tcp Dynamic Port Range
---------------------------------
Start Port      : 10000
Number of Ports : 55535

C:\temp>netstat -anq > ports.txt

roy@rbentley:/mnt/c/temp$ grep 'BOUND' ports.txt | wc -l
10590

roy@rbentley:/mnt/c/temp$ uptime
 13:40:56 up  3:03,  0 users,  load average: 0.52, 0.58, 0.59

Unknown process running on loop

roy@rbentley:/mnt/c/temp$ date
Mon Apr 22 13:47:14 EDT 2019
roy@rbentley:/mnt/c/temp$ ps ux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
roy          6  0.0  0.0  19960  6384 tty1     S    10:37   0:00 -bash
roy        221  0.0  0.0  13768  1428 tty1     S    10:37   0:00 tmux -u -2 -f /usr/share/byobu/profiles/tmuxrc new-session -n - /usr/bin/byobu-shell
roy        254  2.7  0.0  17564  5524 ?        Rs   10:38   5:06 tmux -u -2 -f /usr/share/byobu/profiles/tmuxrc new-session -n - /usr/bin/byobu-shell
roy        258  0.0  0.0  20464  6808 pts/1    Ss   10:38   0:01 /bin/bash
roy      27335  0.0  0.0  14184   880 ?        S    12:58   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy      27337  0.0  0.0      0     0 ?        Z    12:58   0:00 [wget] <defunct>
roy      31511  0.0  0.0  19992  6488 pts/0    Ss   13:03   0:00 /bin/bash
roy      31699  0.0  0.0  19992  6600 pts/2    Ss   13:03   0:00 /bin/bash
roy       3545  2.0  0.0  14184   956 ?        S    13:47   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy       3547  3.0  0.0  20028  2104 ?        S    13:47   0:00 wget -q -O- --tries=1 http://169.254.169.254
roy       3549  0.0  0.0  17380  1920 pts/1    R    13:47   0:00 ps ux
roy@rbentley:/mnt/c/temp$ pstree -hp
init(1)─┬─init(5)───bash(6)───tmux: client(221)
        ├─postgres(32201)─┬─postgres(32218)
        │                 ├─postgres(32219)
        │                 ├─postgres(32220)
        │                 ├─postgres(32221)
        │                 ├─postgres(32222)
        │                 └─postgres(32223)
        ├─timeout(27335)───wget(27337)
        ├─timeout(4738)───wget(4740)
        ├─tmux: server(254)─┬─bash(258)───pstree(4746)
        │                   ├─bash(31511)
        │                   ├─bash(31699)
        │                   ├─sh(4742)───byobu-status(4744)───sed(4747)
        │                   └─sh(4743)───byobu-status(4745)───sed(4748)
        └─{init}(4)
roy@rbentley:/mnt/c/temp$ date
Mon Apr 22 13:47:28 EDT 2019
roy@rbentley:/mnt/c/temp$ ps ux | grep wget | grep -v grep 
roy      27335  0.0  0.0  14184   880 ?        S    12:58   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy      27337  0.0  0.0      0     0 ?        Z    12:58   0:00 [wget] <defunct>
roy       3788  1.0  0.0  14184   956 ?        S    13:47   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy       3790  1.0  0.0  20028  2100 ?        S    13:47   0:00 wget -q -O- --tries=1 http://169.254.169.254
roy@rbentley:/mnt/c/temp$ date
Mon Apr 22 13:47:46 EDT 2019
roy@rbentley:/mnt/c/temp$ ps ux | grep wget | grep -v grep 
roy      27335  0.0  0.0  14184   880 ?        S    12:58   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy      27337  0.0  0.0      0     0 ?        Z    12:58   0:00 [wget] <defunct>
roy       4031  1.0  0.0  14184   960 ?        S    13:47   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy       4033  1.0  0.0  20028  2100 ?        S    13:47   0:00 wget -q -O- --tries=1 http://169.254.169.254
roy@rbentley:/mnt/c/temp$ date
Mon Apr 22 13:48:23 EDT 2019
roy@rbentley:/mnt/c/temp$ ps ux | grep wget | grep -v grep 
roy      27335  0.0  0.0  14184   880 ?        S    12:58   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy      27337  0.0  0.0      0     0 ?        Z    12:58   0:00 [wget] <defunct>
roy       4570  0.0  0.0  14184   956 ?        S    13:48   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy       4573  0.0  0.0  20028  2100 ?        S    13:48   0:00 wget -q -O- --tries=1 http://169.254.169.254
roy@rbentley:/mnt/c/temp$ date
Mon Apr 22 13:52:05 EDT 2019
roy@rbentley:/mnt/c/temp$ ps ux | grep wget | grep -v grep 
roy      27335  0.0  0.0  14184   876 ?        S    12:58   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy      27337  0.0  0.0      0     0 ?        Z    12:58   0:00 [wget] <defunct>
roy       7866  1.0  0.0  14184   956 ?        S    13:52   0:00 timeout 1 wget -q -O- --tries=1 http://169.254.169.254
roy       7868  0.0  0.0  20028  2104 ?        S    13:52   0:00 wget -q -O- --tries=1 http://169.254.169.254

Task Manager view

Looking at the bash terminal doesn't really reflect exactly how quickly it's churning, Task Manager shows a better view.
Task manager

@Brian-Perkins

This comment has been minimized.

Copy link
Collaborator

commented May 1, 2019

Fixed in Windows Insider Build 18890

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.