uvloop icon indicating copy to clipboard operation
uvloop copied to clipboard

Transport sockets do not support shutdown() method

Open Snawoot opened this issue 6 years ago • 9 comments

  • uvloop version: 0.12.2
  • Python version: 3.5.3
  • Platform: Debian 9
  • Can you reproduce the bug with PYTHONASYNCIODEBUG in env?: yes

An attempt to use shutdown method of transport socket causes following exception:

source_traceback: Object created at (most recent call last):
  File "/usr/local/bin/ssh-tarpit", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.5/dist-packages/ssh_tarpit/__main__.py", line 109, in main
    loop.run_until_complete(amain(args, loop))
  File "/usr/lib/python3.5/asyncio/streams.py", line 238, in connection_made
    self._stream_writer)
  File "/usr/local/lib/python3.5/dist-packages/ssh_tarpit/server.py", line 52, in _spawn
    self._loop.create_task(self.handler(reader, writer)))
Traceback (most recent call last):
  File "/usr/lib/python3.5/asyncio/tasks.py", line 239, in _step
    result = coro.send(None)
  File "/usr/lib/python3.5/asyncio/coroutines.py", line 125, in send
    return self.gen.send(value)
  File "/usr/local/lib/python3.5/dist-packages/ssh_tarpit/server.py", line 38, in handler
    sock.shutdown(socket.SHUT_RD)
  File "uvloop/pseudosock.pyx", line 155, in uvloop.loop.PseudoSocket.shutdown
  File "uvloop/pseudosock.pyx", line 19, in uvloop.loop.PseudoSocket._na
TypeError: transport sockets do not support shutdown() method

Such behavior is not observed with default event loop.

Snawoot avatar Mar 23 '19 20:03 Snawoot

Such behavior is not observed with default event loop.

It will be in 3.8 or later. You're using an undocumented way of shutdowning the socket bypassing the top-level transport API.

1st1 avatar Mar 23 '19 20:03 1st1

You should be using transport.write_eof().

1st1 avatar Mar 23 '19 20:03 1st1

@1st1 Hello, thanks for answer!

No, I'm interested in SHUT_RD, so I use sock.shutdown(socket.SHUT_RD) after transport.pause_reading().

Snawoot avatar Mar 23 '19 20:03 Snawoot

Well, there's no API for SHUT_RD in libuv, so uvloop can't support it in any way :(

Why do you need SHUT_RD? It's not a coincidence it's not supported directly in asyncio/twisted; AFAIK it's pretty much useless, because if the the other side continues to send you data you will receive it anyways.

1st1 avatar Mar 23 '19 20:03 1st1

because if the the other side continues to send you data you will receive it anyways

TCP has flow control and sender shall not send more than TCP window size allows him. In this case TCP window will have zero size: Wireshark screenshot

I just tested it with standard Python 3 event loop and everything works as expected. Also I used strace to confirm shutdown syscall was issued and target file descriptor was deregistered from epoll:

12087 getsockname(7, {sa_family=AF_INET, sin_port=htons(2222), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
12087 setsockopt(7, SOL_TCP, TCP_NODELAY, [1], 4) = 0
12087 epoll_wait(3, [], 2, 0)           = 0
12087 epoll_ctl(3, EPOLL_CTL_ADD, 7, {EPOLLIN, {u32=7, u64=94158568030215}}) = 0
12087 epoll_wait(3, [], 3, 0)           = 0
12087 epoll_ctl(3, EPOLL_CTL_DEL, 7, 0x7ffeca04d0d4) = 0
12087 shutdown(7, SHUT_RD)              = 0
12087 getpid()                          = 12087
12087 write(2, "2019-03-23 22:48:58 INFO     TarpitServer: Client ('127.0.0.1', 47216) connected\n", 81) = 81

I consider such behavior as reference behavior: no new events on incoming data will occur, sender transmission will be stalled, data in opposite direction still passing.

shutdown(SHUT_RD) is a valid way to tell system stop accepting new data and potentially free some of associated resources. I may agree: real behavior may vary from system, kernel version, platform and actual socket type, but generally it is integral part of sockets API.

Snawoot avatar Mar 23 '19 22:03 Snawoot

OK, fair enough.

The reason why transport.get_extra_info('socket') does not return a real socket object is because manipulating the socket FD without the loop knowing about it can disrupt the operation of the event loop (even segfault it some cases). This has been implemented in uvloop, and I plan to do the same in asyncio in 3.8 (or 3.9). Therefore, the way you use it to set SHUT_RD can stop working at some point of time. So I'd suggest you to open an issue on bugs.python.org to add a new transport-level asyncio API (e.g. something like transport.shutdown_reading()) to set SHUT_RD for the underlying socket. I can then implement that API in uvloop.

1st1 avatar Mar 24 '19 23:03 1st1

Meanwhile you can try this (untested):

tr_sock = transport.get_extra_info('socket')
sock = socket.socket(tr_sock.family, tr_sock.type, tr_sock.proto, tr_sock.fileno())
try:
    sock.shutdown(socket.SHUT_RD)
finally:
    sock.detach()

1st1 avatar Mar 26 '19 15:03 1st1

Thanks!

Snawoot avatar Mar 26 '19 15:03 Snawoot

For others reference: following code confirmed to work.

    async def handler(self, reader, writer):
        writer.transport.pause_reading()
        sock = writer.transport.get_extra_info('socket')
        if sock is not None:
            try:
                sock.shutdown(socket.SHUT_RD)
            except TypeError:
                direct_sock = socket.socket(sock.family, sock.type, sock.proto, sock.fileno())
                try:
                    direct_sock.shutdown(socket.SHUT_RD)
                finally:
                    direct_sock.detach()

Only disturbing thing is fact EPOLL_CTL_DEL is issued after shutdown:

6053  epoll_wait(3, [], 1024, 0)        = 0
6053  epoll_wait(3, [{EPOLLIN, {u32=12, u64=12}}], 1024, 500) = 1
6053  accept4(12, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK) = 13
6053  setsockopt(13, SOL_TCP, TCP_NODELAY, [1], 4) = 0
6053  accept4(12, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
6053  getsockname(13, {sa_family=AF_INET, sin_port=htons(2222), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 0
6053  getpeername(13, {sa_family=AF_INET, sin_port=htons(38822), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 0
6053  epoll_ctl(3, EPOLL_CTL_ADD, 13, {EPOLLIN, {u32=13, u64=13}}) = 0
6053  epoll_wait(3, [], 1024, 0)        = 0
6053  getsockname(13, {sa_family=AF_INET, sin_port=htons(2222), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 0
6053  shutdown(13, SHUT_RD)             = 0
6053  getpid()                          = 6053
6053  write(2, "2019-03-26 21:41:07 INFO     TarpitServer: Client ('127.0.0.1', 38822) connected\n", 81) = 81
6053  getpid()                          = 6053
6053  epoll_wait(3, [{EPOLLIN, {u32=13, u64=13}}], 1024, 21) = 1
6053  epoll_ctl(3, EPOLL_CTL_DEL, 13, 0x7fff10bf3fa0) = 0
6053  epoll_wait(3, [], 1024, 21)       = 0
6053  getpid()                          = 6053

Presumably transport.pause_reading() works not immediately in uvloop and action taken only when event loop gains execution again. It is not clear, if socket read attempts may occur in that short period (which, in its turn, will definitely fail due to socket new state).

Snawoot avatar Mar 26 '19 20:03 Snawoot