ironpython3 icon indicating copy to clipboard operation
ironpython3 copied to clipboard

Creating socket from descriptor resets timeout on all sockets on same descriptor

Open BCSharp opened this issue 3 years ago • 0 comments

This issue came out during working on #1511.

When a socket is created using a descriptor of another existing socket, the timeout of the existing socket is reset to default (infinite by default). This is because creating a socket around an existing descriptor does not actually create a new system socket, it merely shares it.

Consider the following example in CPython:

Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 24 2018, 00:16:47) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> s = socket.socket(socket.AddressFamily.AF_INET, socket.SocketKind.SOCK_STREAM)
>>> s.gettimeout() # None by default
>>> s.settimeout(1) # change it to 1 s
>>> s.gettimeout()
1.0
>>> s.fileno()
392
>>> s2 = socket.socket(socket.AddressFamily.AF_INET, socket.SocketKind.SOCK_STREAM, 0, s.fileno()) # new socket, same descriptor
>>> s2.gettimeout() # new socket: None (default)
>>> s.gettimeout() # old socket maintains its own timeout
1.0
>>> s2.fileno() # both sockets use the same descriptor
392
>>> s.fileno()
392

Similar code run with IronPython shows that whatever is done to socket s is affecting socket s2:

[.NETCoreApp,Version=v6.0 on .NET 6.0.7 (64-bit)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> s = socket.socket(socket.AddressFamily.AF_INET, socket.SocketKind.SOCK_STREAM)
>>> s.gettimeout() # None by default
>>> s.settimeout(1) # change to 1 s
>>> s.gettimeout()
1.0
>>> s2 = socket.socket(socket.AddressFamily.AF_INET, socket.SocketKind.SOCK_STREAM, 0, s.fileno())  # new socket, same descriptor
>>> s2.gettimeout() # new socket: None (default), as expected
>>> s.gettimeout() # oops, where is 1 s timeout
>>> s.settimeout(1) # let's reset it to 1 s
>>> s2.gettimeout() # now s2 "inherited" the timeout without explicit set
1.0

This may look like an exotic scenario, but it actually happens frequently when using SSL. Here is a typical example:

c = socket.socket(socket.AF_INET)
c.settimeout(0.2)
c = context.wrap_socket(c)

Creating a SSL socket (which derives from socket) by wrapping it around an existing plain socket uses the existing socket descriptor to create the SSL socket. Since both sockets are entangled, creating the SSL socket and setting its timeout to default effectively erases the timeouts set on the underlying socket. It goes like this (ssl.py from StdLib):

socket.__init__(..., fileno=sock.fileno())
self.settimeout(sock.gettimeout())

By the time the timeouts are read out from the supplied socket, there have been already erased during initialization o the clone.

Here are a few ideas how to handle it.

  1. Use constructor Socket(SafeSocketHandle) to create the .NET socket rather than sharing the existing one. I haven't tested it but if it does what I think it does, it should be the best solution. Unfortunately, this constructor is only available in .NET 5 and higher, so still another solution will be also needed.

  2. Patch SSLSocket.__init__ from Stdlib to:

t = sock.gettimeout()
socket.__init__(..., fileno=sock.fileno())
self.settimeout(t)

This will fix the issue of SSL sockets but there may be other places in StdLib that may need to be patched. Also, since StdLib often serves as an example for other libraries, there may be 3rd party libraries that will still have this problem.

  1. Do not set the timeout on the underlying .NET socket to default if a Python socket is being created based on an existing descriptor. Since all sockets sharing the same descriptor are entangled anyway, there is no reason to prefer the default timeout over already pre-existing timeout. I think this solution is the closest to the spirit of the existing IronPython code.

  2. Using the timeout of the socket kept in the Python socket object, set the timeout of the underlying .NET socket to that value explicitly before every operation on the .NET socket that might be affected by the timeout value. This is rather error prone and will require a careful review to make sure that everything is covered but if it works it has the potential to be the closest to CPython's behaviour. Obviously, this makes the entangled sockets not thread-safe, but I think this is already the case with the .NET sockets. Also, I don't know if there are some cases when modifying the timeout of the underlying .NET socket is not allowed.

Any other ideas?

BCSharp avatar Jul 17 '22 18:07 BCSharp