You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SerialPort on Unix: after a device error (e.g. USB unplug), Write with infinite WriteTimeout hangs forever and Close/Dispose spins forever #135369
On Linux, if the serial device fails while I/O is pending (for example, a USB-serial adapter is unplugged), SerialStream's I/O loop exits. It goes through the error.HasValue or POLLNVAL/POLLERR branch, fails the pending requests, and breaks. Neither path clears _ioLoop, so it still points to the finished task.
From then on, that SerialPort instance is permanently wedged:
Write hangs forever.WriteAsync queues a request and calls EnsureIOLoopRunning. That method only starts a loop when _ioLoop == null, so nothing ever processes the request. With the default WriteTimeout (InfiniteTimeout), Write blocks in GetAwaiter().GetResult() forever.
Close/Dispose hang forever.SerialPort.Dispose calls SerialStream.Flush() before Close(). Flush spins until the write queue is empty, which never happens. This occurs even if the writer used a finite WriteTimeout. Cancelling marks the request completed, but only the dead I/O loop removes completed requests from the queue (RemoveCompletedTasks). The cleanup in SerialStream.Dispose (FinishPendingIORequests, releasing the handle) comes after the Flush call and is never reached. The device fd also stays open.
The only way to recover is to restart the process.
Reproduction Steps
Attach a USB-serial adapter on Linux. Nothing needs to be connected to its other end.
Run the following, then unplug the adapter while it is writing:
usingSystem.IO.Ports;varport=newSerialPort(args[0],115200);// WriteTimeout left at default (infinite)port.Open();varwriter=newThread(()=>{while(true){try{port.Write(newbyte[]{0x55},0,1);}catch(Exceptione){Console.WriteLine($"write error: {e.Message}");}Thread.Sleep(50);}}){IsBackground=true};writer.Start();Console.WriteLine("Unplug the adapter, then press Enter");Console.ReadLine();Console.WriteLine("Closing...");port.Close();// never returnsConsole.WriteLine("Closed");
Expected behavior
After the device fails, each Write fails with an exception. Close/Dispose complete and release the handle.
Actual behavior
One write fails with ObjectDisposedException: The port is closed. (or an IOException, if poll itself failed). After that, the next Write blocks forever and Close() never returns.
A live process stuck in this state showed:
_ioLoop was non-null and its task state was RanToCompletion.
_writeQueue.Count == 1.
No thread was running IOLoop.
One thread was blocked in SerialStream.Write → TaskAwaiter.GetResult.
A second thread was in SerialPort.Dispose → SerialStream.Flush, in the SpinWait loop, waking about 1000 times/s.
The fd still pointed at /dev/ttyUSB0 (deleted).
Regression?
Not that we know of. The same exit paths exist on main.
Known Workarounds
None within the process. A finite WriteTimeout frees the writer, but Close/Dispose still hang. Calling code can avoid closing a port that has hit this state, at the cost of leaking the fd, or isolate serial I/O in a child process that can be killed.
Configuration
Linux x64 (Ubuntu), USB-serial adapter (/dev/ttyUSB0)
Linux-specific: the bug is in SerialStream.Unix.cs
Other information
Possible fix: on the two error exits in IOLoop, set _ioLoop = null under _ioLoopLock (as the idle-timeout exit already does). Alternatively, EnsureIOLoopRunning could restart the loop when _ioLoop.IsCompleted. Either way, a later write would start a new loop, which would see the error again and fail the request instead of stranding it.
Related issues:
System.IO.Ports.SerialPort: Under Linux, closing the port while writing cause a deadlock. #100690, "Under Linux, closing the port while writing cause a deadlock". This has the same end state (a write queued with no I/O loop to service it) but a different trigger: the race between Dispose and EnsureIOLoopRunning. Here no concurrent Close is needed; a device error alone strands the loop. That issue's suggested workaround (set a WriteTimeout) doesn't help with the Close/Flush hang described here.
SerialPort turns stale if device is unplugged on linux #121627, "SerialPort turns stale if device is unplugged on linux". Same scenario (Linux unplug). It reports reads and writes silently going nowhere and IsOpen staying true. This issue may be the root cause of some of that behavior, or a more severe form of it.
Related PR:#134854 ("SerialPort.Unix: use UnixHandleAsyncContext", draft) replaces the poll loop with epoll for .NET 12+ Unix targets. Reading the diff, it looks like it would fix this:
There is no write queue or I/O loop.
An epoll error event wakes pending writes, which then fail with EIO.
Flush becomes a zero-byte write that completes after earlier writes fail.
However, older targets keep the existing loop (moved to SerialStream.UnixPollLoop.cs), and this bug is still in it. A targeted fix there would still be needed for consumers on older target frameworks, and may be easier to backport.
Description
On Linux, if the serial device fails while I/O is pending (for example, a USB-serial adapter is unplugged),
SerialStream's I/O loop exits. It goes through theerror.HasValueorPOLLNVAL/POLLERRbranch, fails the pending requests, andbreaks. Neither path clears_ioLoop, so it still points to the finished task.From then on, that
SerialPortinstance is permanently wedged:Writehangs forever.WriteAsyncqueues a request and callsEnsureIOLoopRunning. That method only starts a loop when_ioLoop == null, so nothing ever processes the request. With the defaultWriteTimeout(InfiniteTimeout),Writeblocks inGetAwaiter().GetResult()forever.Close/Disposehang forever.SerialPort.DisposecallsSerialStream.Flush()beforeClose().Flushspins until the write queue is empty, which never happens. This occurs even if the writer used a finiteWriteTimeout. Cancelling marks the request completed, but only the dead I/O loop removes completed requests from the queue (RemoveCompletedTasks). The cleanup inSerialStream.Dispose(FinishPendingIORequests, releasing the handle) comes after theFlushcall and is never reached. The device fd also stays open.The only way to recover is to restart the process.
Reproduction Steps
Expected behavior
After the device fails, each
Writefails with an exception.Close/Disposecomplete and release the handle.Actual behavior
One write fails with
ObjectDisposedException: The port is closed.(or anIOException, ifpollitself failed). After that, the nextWriteblocks forever andClose()never returns.A live process stuck in this state showed:
_ioLoopwas non-null and its task state wasRanToCompletion._writeQueue.Count == 1.IOLoop.SerialStream.Write→TaskAwaiter.GetResult.SerialPort.Dispose→SerialStream.Flush, in theSpinWaitloop, waking about 1000 times/s./dev/ttyUSB0 (deleted).Regression?
Not that we know of. The same exit paths exist on
main.Known Workarounds
None within the process. A finite
WriteTimeoutfrees the writer, butClose/Disposestill hang. Calling code can avoid closing a port that has hit this state, at the cost of leaking the fd, or isolate serial I/O in a child process that can be killed.Configuration
/dev/ttyUSB0)SerialStream.Unix.csOther information
Possible fix: on the two error exits in
IOLoop, set_ioLoop = nullunder_ioLoopLock(as the idle-timeout exit already does). Alternatively,EnsureIOLoopRunningcould restart the loop when_ioLoop.IsCompleted. Either way, a later write would start a new loop, which would see the error again and fail the request instead of stranding it.Related issues:
DisposeandEnsureIOLoopRunning. Here no concurrentCloseis needed; a device error alone strands the loop. That issue's suggested workaround (set aWriteTimeout) doesn't help with theClose/Flushhang described here.IsOpenstaying true. This issue may be the root cause of some of that behavior, or a more severe form of it.Related PR: #134854 ("SerialPort.Unix: use UnixHandleAsyncContext", draft) replaces the poll loop with epoll for .NET 12+ Unix targets. Reading the diff, it looks like it would fix this:
Flushbecomes a zero-byte write that completes after earlier writes fail.However, older targets keep the existing loop (moved to
SerialStream.UnixPollLoop.cs), and this bug is still in it. A targeted fix there would still be needed for consumers on older target frameworks, and may be easier to backport.