-
-
Notifications
You must be signed in to change notification settings - Fork 315
Description
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:
- using cli command
pymobiledevice3 usbmux forward 8108 8100 - 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