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
This issue was filed by claude-opus-5[1m] on behalf of @JPHutchins, who asked that the findings from the smpclient screaming-goblin kickoff session be recorded as issues for durable context. Found while root-causing the integration flake fixed in #128.
_SerialTransportBase.send() discards the return value of pyserial's write():
ifself._framingisNone:
self._conn.write(data) # returns bytes written -- ignoredelse:
forframeinself._framing.encode(data):
self._conn.write(frame) # same
write() is documented to return the number of bytes written. If it ever returns less than len(data), the SMP message goes out truncated, with no error raised anywhere — and for the raw transport that means the server waits forever for a tail that will never arrive.
This is reachable, not theoretical
pyserial's socket:// handler treats a zero write_timeout as non-blocking: it issues a single socket.send() and returns that count without looping.
d=to_bytes(data)
tx_len=length=len(d)
timeout=Timeout(self._write_timeout)
whiletx_len>0:
try:
n=self._socket.send(d)
iftimeout.is_non_blocking:
# Zero timeout indicates non-blocking - simply return the# number of bytes of data actually writtenreturnn
Any remainder is silently dropped. A local serial port masks this because serialposix.write() loops properly, but the transport should not depend on which backend it happens to be bound to.
Note on scope — this is NOT the cause of the #128 flake
I initially diagnosed this as the cause of the mps2_an385.serial_recovery_raw flake and was wrong: setting a non-zero write_timeout alone still failed 2/40 runs. The flake's actual cause was UART RX overrun from an unpaced write burst. This remains a genuine latent hazard on its own merits, which is why it is filed separately rather than claimed in #128.
Suggested fix
Assert the full write, or loop until it completes:
written=self._conn.write(data)
ifwritten!=len(data):
raiseSMPTransportDisconnected(f"short write: {written} of {len(data)} B")
Worth doing alongside #56, since a truncated request is exactly the condition a retransmit policy has to detect.
Warning
LLM Disclosure
This issue was filed by
claude-opus-5[1m]on behalf of @JPHutchins, who asked that the findings from the smpclientscreaming-goblinkickoff session be recorded as issues for durable context. Found while root-causing the integration flake fixed in #128._SerialTransportBase.send()discards the return value ofpyserial'swrite():write()is documented to return the number of bytes written. If it ever returns less thanlen(data), the SMP message goes out truncated, with no error raised anywhere — and for the raw transport that means the server waits forever for a tail that will never arrive.This is reachable, not theoretical
pyserial's
socket://handler treats a zerowrite_timeoutas non-blocking: it issues a singlesocket.send()and returns that count without looping.—
serial/urlhandler/protocol_socket.py(pyserial 3.5)Any remainder is silently dropped. A local serial port masks this because
serialposix.write()loops properly, but the transport should not depend on which backend it happens to be bound to.Note on scope — this is NOT the cause of the #128 flake
I initially diagnosed this as the cause of the
mps2_an385.serial_recovery_rawflake and was wrong: setting a non-zerowrite_timeoutalone still failed 2/40 runs. The flake's actual cause was UART RX overrun from an unpaced write burst. This remains a genuine latent hazard on its own merits, which is why it is filed separately rather than claimed in #128.Suggested fix
Assert the full write, or loop until it completes:
Worth doing alongside #56, since a truncated request is exactly the condition a retransmit policy has to detect.