Skip to content

Forwarding port via cli sometimes diesΒ #1511

@dokisha

Description

@dokisha

Test environment

  • Host OS version: Ubuntu 22
  • Target device model and iOS version: iphone 11 to 16

Describe the bug
Forwarding port (usbmux forward x y) sometimes dies with error Bad file descriptor.

To Reproduce
Steps to reproduce the behavior:

  1. using cli command pymobiledevice3 usbmux forward 8108 8100
  2. forwarding works initially, but dies 10% of the time after several seconds
    Note, I launch two port forwards roughly at the same time, one sometimes dies.
    Service that the port is forwarded to (8100) on device is alive.

Expected behavior
Forwarding doesn't die

Logs

pymobiledevice3 usbmux forward --serial 00008020-001A29A601F8002E 8108 8100
pymobiledevice3.tcp_forwarder[1428] ERROR failed to connect to port: 8100
pymobiledevice3.tcp_forwarder[1428] ERROR failed to connect to port: 8100
pymobiledevice3.tcp_forwarder[1428] INFO connection established from local to remote
pymobiledevice3.tcp_forwarder[1428] ERROR Error reading from socket <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8108), raddr=('127.0.0.1', 49812)>: Connection closed by the peer.
pymobiledevice3.tcp_forwarder[1428] INFO connection <socket.socket [closed] fd=-1, family=AddressFamily.AF_UNIX, type=SocketKind.SOCK_STREAM, proto=0> was closed
pymobiledevice3.tcp_forwarder[1428] ERROR Error reading from socket <socket.socket [closed] fd=-1, family=AddressFamily.AF_UNIX, type=SocketKind.SOCK_STREAM, proto=0>: [Errno 9] Bad file descriptor
pymobiledevice3.tcp_forwarder[1428] INFO Closing everything

Traceback (most recent call last):
 File "/pmd3/pymobiledevice3/tcp_forwarder.py", line 101, in _handle_data
   data = from_sock.recv(1024)

OSError: [Errno 9] Bad file descriptor
During handling of the above exception, another exception occurred:

Traceback (most recent call last):
 File "/usr/local/bin/pymobiledevice3", line 7, in <module>
   sys.exit(main())
 File "/pmd3/pymobiledevice3/__main__.py", line 275, in main
   while invoke_cli_with_error_handling():
 File "/pmd3/pymobiledevice3/__main__.py", line 192, in invoke_cli_with_error_handling
   cli()
 File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1161, in __call__
   return self.main(*args, **kwargs)
 File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1082, in main
   rv = self.invoke(ctx)
 File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1697, in invoke
   return _process_result(sub_ctx.command.invoke(sub_ctx))
 File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1697, in invoke
   return _process_result(sub_ctx.command.invoke(sub_ctx))
 File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 1443, in invoke
   return ctx.invoke(self.callback, **ctx.params)
 File "/usr/local/lib/python3.9/dist-packages/click/core.py", line 788, in invoke
   return __callback(*args, **kwargs)
 File "/pmd3/pymobiledevice3/cli/usbmux.py", line 45, in usbmux_forward
   forwarder.start()
 File "/pmd3/pymobiledevice3/tcp_forwarder.py", line 71, in start
   self._handle_data(current_sock, closed_sockets)
 File "/pmd3/pymobiledevice3/tcp_forwarder.py", line 109, in _handle_data
   self._handle_close_or_error(from_sock)
 File "/pmd3/pymobiledevice3/tcp_forwarder.py", line 93, in _handle_close_or_error
   self.inputs.remove(other_sock)

ValueError: list.remove(x): x not in list

Additional context
Asked Devin what he thinks is going on, not sure if he's right though, just more context.

Answer:
This is a bug in the TcpForwarder error handling. The issue occurs when a socket is removed from the inputs list multiple times, causing a ValueError when trying to remove an already-removed socket. (tcp_forwarder.py:87-96)

The Bug
The error happens in _handle_close_or_error() at line 93 where it tries to remove sockets from self.inputs: (tcp_forwarder.py:93-94)

self.inputs.remove(other_sock)  
self.inputs.remove(from_sock)

The problem is that when a socket error occurs, the socket might already be removed from the inputs list in a previous iteration or through another code path. The closed_sockets set is used to track closed sockets, but there's a race condition. (tcp_forwarder.py:69-76)

The Fix
The fix is to check if the socket exists in the list before removing it. Change lines 93-94 in _handle_close_or_error(): (tcp_forwarder.py:93-96)

From:

self.inputs.remove(other_sock)  
self.inputs.remove(from_sock)

To:

if other_sock in self.inputs:  
    self.inputs.remove(other_sock)  
if from_sock in self.inputs:  
    self.inputs.remove(from_sock)

Why This Happens
The forwarder uses select() to monitor multiple sockets and when one closes, it triggers cleanup. However, if both sockets in a connection pair encounter errors around the same time, the cleanup can be attempted twice for the same socket. tcp_forwarder.py:55-80

The closed_sockets set is meant to prevent this, but the check at line 69 only prevents processing sockets that are already in closed_sockets, not the removal operation itself. (tcp_forwarder.py:69-76)

Notes
This is a defensive programming issue - the code assumes sockets are always present in the inputs list when trying to remove them. The fix makes the cleanup operation idempotent, which is safer for concurrent socket operations. (tcp_forwarder.py:87-96)

For community

⬇️ Please click the πŸ‘ reaction instead of leaving a +1 or πŸ‘ comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions