fix(r2-01): classify ISO-on-TCP framing/desync faults as connection-fatal (STAB-15a, task 7)

This commit is contained in:
Joseph Doherty
2026-07-13 09:58:11 -04:00
parent 8aed9ac365
commit dab0029add
2 changed files with 26 additions and 6 deletions
@@ -46,7 +46,7 @@
{ {
"id": 7, "id": 7,
"subject": "GREEN: broaden IsS7ConnectionFatal to the ISO-on-TCP framing surface (NOT InvalidDataException) + xmldoc (STAB-15a)", "subject": "GREEN: broaden IsS7ConnectionFatal to the ISO-on-TCP framing surface (NOT InvalidDataException) + xmldoc (STAB-15a)",
"status": "pending", "status": "completed",
"blockedBy": [6] "blockedBy": [6]
}, },
{ {
@@ -1256,10 +1256,24 @@ public sealed class S7Driver
/// <summary> /// <summary>
/// True when <paramref name="ex"/> (or any inner exception) is a socket-level / connection /// True when <paramref name="ex"/> (or any inner exception) is a socket-level / connection
/// loss that a reopen can repair — a <see cref="System.Net.Sockets.SocketException"/> / /// loss OR an ISO-on-TCP framing/desync fault that a reopen can repair — a
/// <see cref="System.IO.IOException"/> / <see cref="ObjectDisposedException"/>, or an S7.Net /// <see cref="System.Net.Sockets.SocketException"/> / <see cref="System.IO.IOException"/> /
/// <see cref="PlcException"/> carrying <see cref="ErrorCode.ConnectionError"/>. A /// <see cref="ObjectDisposedException"/>; an S7.Net framing violation
/// data-address / type error (S7.Net's <see cref="ErrorCode.ReadData"/> / /// (<see cref="TPKTInvalidException"/> / <see cref="TPDUInvalidException"/> /
/// <see cref="WrongNumberOfBytesException"/>); or a <see cref="PlcException"/> carrying
/// <see cref="ErrorCode.ConnectionError"/> or <see cref="ErrorCode.WrongNumberReceivedBytes"/>.
/// A framing violation means the single serialized stream's position is untrustworthy — a
/// half-read PDU left by a timeout/cancellation makes every later response mis-frame, so only
/// a reopen recovers (STAB-15a: the Modbus STAB-3 desync mode rebuilt in S7).
/// <para>
/// Deliberately NOT <see cref="System.IO.InvalidDataException"/>: the driver's own decode
/// backstop (<see cref="ReinterpretRawValue"/> / <see cref="DecodeScalarBlock"/>) throws
/// it for a declared-type/address-size CONFIG mismatch on a HEALTHY socket — classifying
/// it would churn a full reopen on every read of a mis-authored tag. A genuinely desynced
/// stream's very next PDU still lands in TPKT/TPDU/WrongNumber territory, which now tears
/// down, so a real desync self-heals within one extra failed call.
/// </para>
/// A data-address / type error (S7.Net's <see cref="ErrorCode.ReadData"/> /
/// <see cref="ErrorCode.WriteData"/> for a bad address, PUT/GET-denied, etc.) is deliberately /// <see cref="ErrorCode.WriteData"/> for a bad address, PUT/GET-denied, etc.) is deliberately
/// NOT treated as fatal — reopening wouldn't help and would churn a healthy connection. /// NOT treated as fatal — reopening wouldn't help and would churn a healthy connection.
/// </summary> /// </summary>
@@ -1271,7 +1285,13 @@ public sealed class S7Driver
{ {
if (e is System.Net.Sockets.SocketException or System.IO.IOException or ObjectDisposedException) if (e is System.Net.Sockets.SocketException or System.IO.IOException or ObjectDisposedException)
return true; return true;
if (e is PlcException { ErrorCode: ErrorCode.ConnectionError }) // ISO-on-TCP framing/desync surface (STAB-15a): the stream position is untrustworthy —
// a half-read PDU left by a timeout/cancellation makes every later response mis-frame.
// NOTE: deliberately NOT System.IO.InvalidDataException — ReinterpretRawValue throws it
// for a declared-type/address-size CONFIG mismatch on a healthy socket.
if (e is TPKTInvalidException or TPDUInvalidException or WrongNumberOfBytesException)
return true;
if (e is PlcException { ErrorCode: ErrorCode.ConnectionError or ErrorCode.WrongNumberReceivedBytes })
return true; return true;
} }
return false; return false;