From fe635ef180a7a9948d0aabec325819f3ba722fff Mon Sep 17 00:00:00 2001 From: Devedse Date: Thu, 16 Apr 2026 14:48:01 +0200 Subject: [PATCH 1/2] iSCSI: Add NOP-Out keepalive and handle target-initiated NOP-In MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During idle periods an iSCSI target can close the session due to the DefaultTime2Retain/DefaultTime2Wait timeout expiring while no traffic is flowing. This causes the next I/O to fail with a broken connection rather than a clean retry. Changes in Connection.cs: 1. SemaphoreSlim (_streamSemaphore) - serialises all PDU traffic on the NetworkStream so the keepalive timer callback and the main Send / SendAsync / Logout paths can never interleave writes or reads on the same socket. 2. Keepalive timer (_keepAliveTimer) - fires every 10 seconds after a successful login and calls KeepAliveCallback. The callback skips the tick if the semaphore is already held (a real command is in flight), so it adds zero latency to normal operation. 3. SendNopOut - issues an initiator-initiated NOP-Out PDU with ITT=0xFFFFFFFF and TTT=0xFFFFFFFF (RFC 3720 §10.18). With these reserved values the target MUST NOT send a NOP-In response, so there is no read-side traffic to worry about. 4. HandleNopIn - when the target itself initiates a NOP-In (TTT != 0xFFFFFFFF) the initiator is required to respond with a NOP-Out that echoes the TTT (RFC 3720 §10.19). Previously DiscUtils had no handler for this opcode, so a target-initiated ping would surface as an unrecognised PDU error. The StatSN carried by a target-initiated NOP-In is informational and does NOT consume a sequence number, so SeenStatusSequenceNumber() is deliberately NOT called. 5. ReadPdu / ReadPduAsync - loop over consecutive NOP-In PDUs (up to 16) before returning the next real command response, which keeps the async path correct without changing any public API. --- Library/DiscUtils.Iscsi/Connection.cs | 229 +++++++++++++++++++++++--- 1 file changed, 206 insertions(+), 23 deletions(-) diff --git a/Library/DiscUtils.Iscsi/Connection.cs b/Library/DiscUtils.Iscsi/Connection.cs index 0b4abff8..4042fc0e 100644 --- a/Library/DiscUtils.Iscsi/Connection.cs +++ b/Library/DiscUtils.Iscsi/Connection.cs @@ -46,6 +46,17 @@ internal sealed class Connection : IDisposable private readonly NetworkStream _stream; + /// + /// Semaphore to synchronize access to the network stream. NOP-Out responses from the + /// keepalive timer and normal Send/ReadPdu traffic must not interleave. + /// + private readonly SemaphoreSlim _streamSemaphore = new(1, 1); + + /// + /// Timer that periodically sends NOP-Out pings to keep the iSCSI session alive. + /// + private Timer _keepAliveTimer; + public Connection(Session session, TargetAddress address, Authenticator[] authenticators) { Session = session; @@ -80,6 +91,11 @@ public Connection(Session session, TargetAddress address, Authenticator[] authen _negotiatedParameters = []; NegotiateSecurity(); NegotiateFeatures(); + + // Start keepalive timer after login completes. + // Send a NOP-Out every 10 seconds to prevent the target from + // timing out the session during idle periods. + _keepAliveTimer = new Timer(KeepAliveCallback, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); } internal LoginStages CurrentLoginStage { get; private set; } = LoginStages.SecurityNegotiation; @@ -106,17 +122,30 @@ public void Close(LogoutReason reason) { try { - var req = new LogoutRequest(this); - var packet = req.GetBytes(reason); - _stream.Write(packet, 0, packet.Length); - _stream.Flush(); + _streamSemaphore.Wait(); + try + { + // Stop the keepalive timer while holding the semaphore + // to ensure no callback is in-flight. + _keepAliveTimer?.Dispose(); + _keepAliveTimer = null; - var pdu = ReadPdu(); - var resp = ParseResponse(pdu); + var req = new LogoutRequest(this); + var packet = req.GetBytes(reason); + _stream.Write(packet, 0, packet.Length); + _stream.Flush(); + + var pdu = ReadPdu(); + var resp = ParseResponse(pdu); - if (resp.Response != LogoutResponseCode.ClosedSuccessfully) + if (resp.Response != LogoutResponseCode.ClosedSuccessfully) + { + throw new InvalidProtocolException($"Target indicated failure during logout: {resp.Response}"); + } + } + finally { - throw new InvalidProtocolException($"Target indicated failure during logout: {resp.Response}"); + _streamSemaphore.Release(); } } catch (EndOfStreamException) @@ -133,6 +162,61 @@ public void Close(LogoutReason reason) } } + /// + /// Timer callback: sends a NOP-Out to keep the iSCSI session alive. + /// Uses ITT=0xFFFFFFFF and TTT=0xFFFFFFFF so the target does not send a NOP-In response + /// (RFC 3720 §10.18), avoiding read-side contention with the main Send path. + /// + private void KeepAliveCallback(object state) + { + if (!_streamSemaphore.Wait(0)) + { + // A Send or Close is in progress; skip this ping and retry on the next timer tick. + return; + } + + try + { + SendNopOut(); + } + catch (IOException) + { + // Connection is dead — the next Send will surface the error. + } + catch (ObjectDisposedException) + { + // Stream was disposed — connection is shutting down. + } + finally + { + _streamSemaphore.Release(); + } + } + + /// + /// Sends an initiator-initiated NOP-Out with ITT=0xFFFFFFFF and TTT=0xFFFFFFFF. + /// This is a "ping" that keeps the iSCSI session alive without requiring a response. + /// + private void SendNopOut() + { + var nopOut = new byte[48]; + // Byte 0: Immediate (0x40) | OpCode NopOut (0x00) + nopOut[0] = 0x40; + // Byte 1: Final bit + nopOut[1] = 0x80; + // Bytes 16-19: ITT = 0xFFFFFFFF (no response expected) + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16); + // Bytes 20-23: TTT = 0xFFFFFFFF + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 20); + // Bytes 24-27: CmdSN (not incremented for immediate PDUs) + EndianUtilities.WriteBytesBigEndian(Session.CommandSequenceNumber, nopOut, 24); + // Bytes 28-31: ExpStatSN + EndianUtilities.WriteBytesBigEndian(ExpectedStatusSequenceNumber, nopOut, 28); + + _stream.Write(nopOut, 0, nopOut.Length); + _stream.Flush(); + } + /// /// Sends an SCSI command (aka task) to a LUN via the connected target. /// @@ -142,6 +226,9 @@ public void Close(LogoutReason reason) /// The number of bytes received. public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuffer) { + _streamSemaphore.Wait(); + try + { for (var i = 0; ; i++) { try @@ -281,6 +368,11 @@ public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuff { } } + } + finally + { + _streamSemaphore.Release(); + } } /// @@ -293,6 +385,9 @@ public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuff /// The number of bytes received. public async ValueTask SendAsync(ScsiCommand cmd, ReadOnlyMemory outBuffer, Memory inBuffer, CancellationToken cancellationToken) { + await _streamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { for (var i = 0; ; i++) { try @@ -410,6 +505,11 @@ public async ValueTask SendAsync(ScsiCommand cmd, ReadOnlyMemory outB { } } + } + finally + { + _streamSemaphore.Release(); + } } public T Send(ScsiCommand cmd, ReadOnlySpan buffer, int expected) @@ -816,32 +916,115 @@ private void NegotiateFeatures() private ProtocolDataUnit ReadPdu() { - var pdu = ProtocolDataUnit.ReadFrom(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None); - - if (pdu.OpCode == OpCode.Reject) + const int MaxNopInRetries = 16; + for (var nopCount = 0; ; nopCount++) { - var pkt = new RejectPacket(); - pkt.Parse(pdu); + var pdu = ProtocolDataUnit.ReadFrom(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None); - throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); - } + if (pdu.OpCode == OpCode.Reject) + { + var pkt = new RejectPacket(); + pkt.Parse(pdu); + + throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); + } - return pdu; + // RFC 3720 §10.19: Handle target-initiated NOP-In (ping). + // Respond with NOP-Out echoing the Target Transfer Tag, then + // continue reading the next PDU. + if (pdu.OpCode == OpCode.NopIn) + { + if (nopCount >= MaxNopInRetries) + { + throw new InvalidProtocolException($"Received {nopCount} consecutive NOP-In PDUs without a command response"); + } + + HandleNopIn(pdu); + continue; + } + + return pdu; + } } private async ValueTask ReadPduAsync(CancellationToken cancellationToken) { - var pdu = await ProtocolDataUnit.ReadFromAsync(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None, cancellationToken).ConfigureAwait(false); - - if (pdu.OpCode == OpCode.Reject) + const int MaxNopInRetries = 16; + for (var nopCount = 0; ; nopCount++) { - var pkt = new RejectPacket(); - pkt.Parse(pdu); + var pdu = await ProtocolDataUnit.ReadFromAsync(_stream, HeaderDigest != Digest.None, DataDigest != Digest.None, cancellationToken).ConfigureAwait(false); + + if (pdu.OpCode == OpCode.Reject) + { + var pkt = new RejectPacket(); + pkt.Parse(pdu); + + throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); + } + + if (pdu.OpCode == OpCode.NopIn) + { + if (nopCount >= MaxNopInRetries) + { + throw new InvalidProtocolException($"Received {nopCount} consecutive NOP-In PDUs without a command response"); + } + + HandleNopIn(pdu); + continue; + } - throw new IscsiException($"Target sent reject packet, reason {pkt.Reason}"); + return pdu; } + } + + /// + /// Responds to a target-initiated NOP-In with a NOP-Out (RFC 3720 §10.18/10.19). + /// Target-initiated NOP-In has TTT != 0xFFFFFFFF and requires a NOP-Out reply. + /// The StatSN in a target-initiated NOP-In is informational and does NOT consume + /// a sequence number, so we must NOT call SeenStatusSequenceNumber here. + /// + private void HandleNopIn(ProtocolDataUnit pdu) + { + var headerData = pdu.HeaderData; - return pdu; + // Target Transfer Tag at offset 20 + var targetTransferTag = EndianUtilities.ToUInt32BigEndian(headerData.AsSpan(20)); + + // If TTT is 0xFFFFFFFF, this is a response to our own NOP-Out (unsolicited); + // no reply needed. + if (targetTransferTag == 0xFFFFFFFF) + { + return; + } + + // LUN at offset 8 + var lun = EndianUtilities.ToUInt64BigEndian(headerData.AsSpan(8)); + + // NOTE: We intentionally do NOT call SeenStatusSequenceNumber() here. + // RFC 3720 §10.19: A target-initiated NOP-In carries a StatSN, but this + // StatSN is NOT consumed — it's the same value the target will use for the + // next real command response. Advancing ExpectedStatusSequenceNumber here + // would cause the next ParseResponse to fail with a sequence mismatch. + + // Build NOP-Out response (RFC 3720 §10.18) + var nopOut = new byte[48]; + // Byte 0: Immediate bit (0x40) | OpCode NopOut (0x00) + nopOut[0] = 0x40; + // Byte 1: Final bit (0x80) + nopOut[1] = 0x80; + // Bytes 8-15: LUN (copy from NOP-In) + EndianUtilities.WriteBytesBigEndian(lun, nopOut, 8); + // Bytes 16-19: Initiator Task Tag = 0xFFFFFFFF (response to target ping) + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16); + // Bytes 20-23: Target Transfer Tag (echo from NOP-In) + EndianUtilities.WriteBytesBigEndian(targetTransferTag, nopOut, 20); + // Bytes 24-27: CmdSN (not consumed for immediate PDUs with ITT=0xFFFFFFFF) + EndianUtilities.WriteBytesBigEndian(Session.CommandSequenceNumber, nopOut, 24); + // Bytes 28-31: ExpStatSN + EndianUtilities.WriteBytesBigEndian(ExpectedStatusSequenceNumber, nopOut, 28); + + _stream.Write(nopOut, 0, nopOut.Length); + _stream.Flush(); } private void GetParametersToNegotiate(TextBuffer parameters, KeyUsagePhase phase, SessionType sessionType) From 2c53fa741bcbd6ce3a7b9e126bb147756c564833 Mon Sep 17 00:00:00 2001 From: LTRData Date: Fri, 17 Apr 2026 23:18:00 +0200 Subject: [PATCH 2/2] Minor optimizations etc --- Library/DiscUtils.Iscsi/Connection.cs | 434 ++++++++++++++------------ 1 file changed, 226 insertions(+), 208 deletions(-) diff --git a/Library/DiscUtils.Iscsi/Connection.cs b/Library/DiscUtils.Iscsi/Connection.cs index 4042fc0e..173c82ee 100644 --- a/Library/DiscUtils.Iscsi/Connection.cs +++ b/Library/DiscUtils.Iscsi/Connection.cs @@ -26,6 +26,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Sockets; @@ -95,7 +96,10 @@ public Connection(Session session, TargetAddress address, Authenticator[] authen // Start keepalive timer after login completes. // Send a NOP-Out every 10 seconds to prevent the target from // timing out the session during idle periods. - _keepAliveTimer = new Timer(KeepAliveCallback, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); + _keepAliveTimer = new Timer(callback: KeepAliveCallback, + state: null, + dueTime: TimeSpan.FromSeconds(10), + period: TimeSpan.FromSeconds(10)); } internal LoginStages CurrentLoginStage { get; private set; } = LoginStages.SecurityNegotiation; @@ -167,7 +171,7 @@ public void Close(LogoutReason reason) /// Uses ITT=0xFFFFFFFF and TTT=0xFFFFFFFF so the target does not send a NOP-In response /// (RFC 3720 §10.18), avoiding read-side contention with the main Send path. /// - private void KeepAliveCallback(object state) + private async void KeepAliveCallback(object _) { if (!_streamSemaphore.Wait(0)) { @@ -177,7 +181,11 @@ private void KeepAliveCallback(object state) try { - SendNopOut(); +#if DEBUG + Trace.WriteLine("Sending keep-alive..."); +#endif + + await SendNopOutAsync().ConfigureAwait(false); } catch (IOException) { @@ -193,28 +201,36 @@ private void KeepAliveCallback(object state) } } + private byte[] nopOut; + /// /// Sends an initiator-initiated NOP-Out with ITT=0xFFFFFFFF and TTT=0xFFFFFFFF. /// This is a "ping" that keeps the iSCSI session alive without requiring a response. /// - private void SendNopOut() + private async ValueTask SendNopOutAsync() { - var nopOut = new byte[48]; - // Byte 0: Immediate (0x40) | OpCode NopOut (0x00) - nopOut[0] = 0x40; - // Byte 1: Final bit - nopOut[1] = 0x80; - // Bytes 16-19: ITT = 0xFFFFFFFF (no response expected) - EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16); - // Bytes 20-23: TTT = 0xFFFFFFFF - EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 20); + if (nopOut is null) + { + nopOut = new byte[48]; + + // Byte 0: Immediate (0x40) | OpCode NopOut (0x00) + nopOut[0] = 0x40; + // Byte 1: Final bit + nopOut[1] = 0x80; + // Bytes 16-19: ITT = 0xFFFFFFFF (no response expected) + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 16); + // Bytes 20-23: TTT = 0xFFFFFFFF + EndianUtilities.WriteBytesBigEndian(0xFFFFFFFF, nopOut, 20); + } + // Bytes 24-27: CmdSN (not incremented for immediate PDUs) EndianUtilities.WriteBytesBigEndian(Session.CommandSequenceNumber, nopOut, 24); // Bytes 28-31: ExpStatSN EndianUtilities.WriteBytesBigEndian(ExpectedStatusSequenceNumber, nopOut, 28); - _stream.Write(nopOut, 0, nopOut.Length); - _stream.Flush(); + await _stream.WriteAsync(nopOut).ConfigureAwait(false); + + await _stream.FlushAsync().ConfigureAwait(false); } /// @@ -227,148 +243,149 @@ private void SendNopOut() public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuffer) { _streamSemaphore.Wait(); + try { - for (var i = 0; ; i++) - { - try + for (var i = 0; ; i++) { - // RFC 3720 Bug #8a: Allocate new Task Tag for this command - // Originally added because we thought each command must have a unique ITT - // TESTING RESULT: NOT NECESSARY - Test passes without this call - // The Session already maintains ITT correctly without explicit increment here - // Leaving commented for reference in case unique ITT per command is needed in future - //Session.NextTaskTag(); + try + { + // RFC 3720 Bug #8a: Allocate new Task Tag for this command + // Originally added because we thought each command must have a unique ITT + // TESTING RESULT: NOT NECESSARY - Test passes without this call + // The Session already maintains ITT correctly without explicit increment here + // Leaving commented for reference in case unique ITT per command is needed in future + //Session.NextTaskTag(); - // RFC 3720: DataSN starts at 0 for each new command - var expectedDataSN = 0u; + // RFC 3720: DataSN starts at 0 for each new command + var expectedDataSN = 0u; - var req = new CommandRequest(this, cmd.TargetLun); + var req = new CommandRequest(this, cmd.TargetLun); - var outBufferCount = outBuffer.Length; - var inBufferMax = inBuffer.Length; + var outBufferCount = outBuffer.Length; + var inBufferMax = inBuffer.Length; - var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0); + var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0); - // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow". - // Even if we're sending < outBufferCount, we set F=1 because any remaining data - // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out. + // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow". + // Even if we're sending < outBufferCount, we set F=1 because any remaining data + // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out. - // Simpler logic: Use buffer sizes directly - var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax; + // Simpler logic: Use buffer sizes directly + var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax; - var packet = req.GetBytes(cmd, - immediateData: outBuffer.Slice(0, toSend), - isFinalData: true, - willRead: inBufferMax != 0, - willWrite: outBufferCount != 0, - expected: expectedTransferLength); + var packet = req.GetBytes(cmd, + immediateData: outBuffer.Slice(0, toSend), + isFinalData: true, + willRead: inBufferMax != 0, + willWrite: outBufferCount != 0, + expected: expectedTransferLength); - _stream.Write(packet, 0, packet.Length); - _stream.Flush(); + _stream.Write(packet, 0, packet.Length); + _stream.Flush(); - var numSent = toSend; - while (numSent < outBufferCount) - { - var pdu = ReadPdu(); + var numSent = toSend; + while (numSent < outBufferCount) + { + var pdu = ReadPdu(); - var resp = ParseResponse(pdu); - var numApproved = (int)resp.DesiredTransferLength; - var targetTransferTag = resp.TargetTransferTag; + var resp = ParseResponse(pdu); + var numApproved = (int)resp.DesiredTransferLength; + var targetTransferTag = resp.TargetTransferTag; - // DataSN resets to 0 for each R2T (not continuous across multiple R2Ts) - var pktsSent = 0; - while (numApproved > 0) - { - toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength); - - var pkt = new DataOutPacket(this, cmd.TargetLun); - var currentDataSN = pktsSent++; - - packet = pkt.GetBytes(data: outBuffer.Slice(numSent, toSend), - isFinalData: toSend == numApproved, - dataSeqNumber: currentDataSN, - bufferOffset: (uint)numSent, - targetTransferTag: targetTransferTag); - - _stream.Write(packet, 0, packet.Length); - _stream.Flush(); - - numApproved -= toSend; - numSent += toSend; - } - } + // DataSN resets to 0 for each R2T (not continuous across multiple R2Ts) + var pktsSent = 0; + while (numApproved > 0) + { + toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength); - var isFinal = false; - var numRead = 0; - while (!isFinal) - { - var pdu = ReadPdu(); + var pkt = new DataOutPacket(this, cmd.TargetLun); + var currentDataSN = pktsSent++; + + packet = pkt.GetBytes(data: outBuffer.Slice(numSent, toSend), + isFinalData: toSend == numApproved, + dataSeqNumber: currentDataSN, + bufferOffset: (uint)numSent, + targetTransferTag: targetTransferTag); + + _stream.Write(packet, 0, packet.Length); + _stream.Flush(); - if (pdu.OpCode == OpCode.ScsiResponse) + numApproved -= toSend; + numSent += toSend; + } + } + + var isFinal = false; + var numRead = 0; + while (!isFinal) { - var resp = ParseResponse(pdu); + var pdu = ReadPdu(); - if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition) + if (pdu.OpCode == OpCode.ScsiResponse) { - var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0); - var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray(); + var resp = ParseResponse(pdu); - if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired) + if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition) { - goto retry; - } + var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0); + var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray(); - throw new ScsiCommandException(resp.Status, senseData); - } + if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired) + { + goto retry; + } - if (resp.StatusPresent && resp.Status != ScsiStatus.Good) - { - throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); - } + throw new ScsiCommandException(resp.Status, senseData); + } - isFinal = resp.Header.FinalPdu; - } - else if (pdu.OpCode == OpCode.ScsiDataIn) - { - var resp = ParseResponse(pdu); + if (resp.StatusPresent && resp.Status != ScsiStatus.Good) + { + throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); + } - // RFC 3720 Section 10.7.4: Validate DataSN sequence - if (resp.DataSequenceNumber != expectedDataSN) - { - throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}"); + isFinal = resp.Header.FinalPdu; } - - if (resp.StatusPresent && resp.Status != ScsiStatus.Good) + else if (pdu.OpCode == OpCode.ScsiDataIn) { - throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); - } + var resp = ParseResponse(pdu); - if (resp.ReadData != null) - { - resp.ReadData.AsSpan().CopyTo(inBuffer.Slice((int)resp.BufferOffset)); - numRead += resp.ReadData.Length; - } + // RFC 3720 Section 10.7.4: Validate DataSN sequence + if (resp.DataSequenceNumber != expectedDataSN) + { + throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}"); + } + + if (resp.StatusPresent && resp.Status != ScsiStatus.Good) + { + throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); + } + + if (resp.ReadData != null) + { + resp.ReadData.AsSpan().CopyTo(inBuffer.Slice((int)resp.BufferOffset)); + numRead += resp.ReadData.Length; + } - isFinal = resp.Header.FinalPdu; + isFinal = resp.Header.FinalPdu; - expectedDataSN++; + expectedDataSN++; + } } - } - return Math.Max(numRead, numSent); - } - finally - { - Session.NextTaskTag(); - Session.NextCommandSequenceNumber(); - } + return Math.Max(numRead, numSent); + } + finally + { + Session.NextTaskTag(); + Session.NextCommandSequenceNumber(); + } - retry: - { + retry: + { + } } } - } finally { _streamSemaphore.Release(); @@ -386,126 +403,127 @@ public int Send(ScsiCommand cmd, ReadOnlySpan outBuffer, Span inBuff public async ValueTask SendAsync(ScsiCommand cmd, ReadOnlyMemory outBuffer, Memory inBuffer, CancellationToken cancellationToken) { await _streamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - for (var i = 0; ; i++) - { - try + for (var i = 0; ; i++) { - // RFC 3720: DataSN starts at 0 for each new command - var expectedDataSN = 0u; + try + { + // RFC 3720: DataSN starts at 0 for each new command + var expectedDataSN = 0u; - var req = new CommandRequest(this, cmd.TargetLun); + var req = new CommandRequest(this, cmd.TargetLun); - var outBufferCount = outBuffer.Length; - var inBufferMax = inBuffer.Length; + var outBufferCount = outBuffer.Length; + var inBufferMax = inBuffer.Length; - var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0); + var toSend = Math.Min(outBufferCount, Session.ImmediateData ? Session.FirstBurstLength : 0); - // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow". - // Even if we're sending < outBufferCount, we set F=1 because any remaining data - // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out. + // F bit (isFinalData) = true means "no more unsolicited Data-Out PDUs will follow". + // Even if we're sending < outBufferCount, we set F=1 because any remaining data + // will be sent via solicited Data-Out (after R2T), not more unsolicited Data-Out. - // Simpler logic: Use buffer sizes directly - var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax; + // Simpler logic: Use buffer sizes directly + var expectedTransferLength = outBufferCount != 0 ? (uint)outBufferCount : (uint)inBufferMax; - var packet = req.GetBytes(cmd, outBuffer.Span.Slice(0, toSend), true, inBufferMax != 0, outBufferCount != 0, expectedTransferLength); - await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false); - await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); - var numSent = toSend; - var pktsSent = 0; - while (numSent < outBufferCount) - { - var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false); + var packet = req.GetBytes(cmd, outBuffer.Span.Slice(0, toSend), true, inBufferMax != 0, outBufferCount != 0, expectedTransferLength); + await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false); + await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); + var numSent = toSend; + var pktsSent = 0; + while (numSent < outBufferCount) + { + var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false); - var resp = ParseResponse(pdu); - var numApproved = (int)resp.DesiredTransferLength; - var targetTransferTag = resp.TargetTransferTag; + var resp = ParseResponse(pdu); + var numApproved = (int)resp.DesiredTransferLength; + var targetTransferTag = resp.TargetTransferTag; - while (numApproved > 0) - { - toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength); + while (numApproved > 0) + { + toSend = Math.Min(Math.Min(outBufferCount - numSent, numApproved), MaxTargetReceiveDataSegmentLength ?? MaxInitiatorTransmitDataSegmentLength); - var pkt = new DataOutPacket(this, cmd.TargetLun); - var currentDataSN = pktsSent++; - packet = pkt.GetBytes(outBuffer.Span.Slice(numSent, toSend), toSend == numApproved, currentDataSN, (uint)numSent, targetTransferTag); - await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false); - await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); + var pkt = new DataOutPacket(this, cmd.TargetLun); + var currentDataSN = pktsSent++; + packet = pkt.GetBytes(outBuffer.Span.Slice(numSent, toSend), toSend == numApproved, currentDataSN, (uint)numSent, targetTransferTag); + await _stream.WriteAsync(packet, cancellationToken).ConfigureAwait(false); + await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); - numApproved -= toSend; - numSent += toSend; + numApproved -= toSend; + numSent += toSend; + } } - } - var isFinal = false; - var numRead = 0; - while (!isFinal) - { - var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false); - - if (pdu.OpCode == OpCode.ScsiResponse) + var isFinal = false; + var numRead = 0; + while (!isFinal) { - var resp = ParseResponse(pdu); + var pdu = await ReadPduAsync(cancellationToken).ConfigureAwait(false); - if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition) + if (pdu.OpCode == OpCode.ScsiResponse) { - var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0); - var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray(); + var resp = ParseResponse(pdu); - if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired) + if (resp.StatusPresent && resp.Status == ScsiStatus.CheckCondition) { - goto retry; + var senseLength = EndianUtilities.ToUInt16BigEndian(pdu.ContentData, 0); + var senseData = pdu.ContentData.AsSpan(2, senseLength).ToArray(); + + if (i == 0 && ScsiSenseParser.TryParse(senseData, out var sense) && sense.IndicatesRetryRequired) + { + goto retry; + } + + throw new ScsiCommandException(resp.Status, senseData); } - throw new ScsiCommandException(resp.Status, senseData); - } + if (resp.StatusPresent && resp.Status != ScsiStatus.Good) + { + throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); + } - if (resp.StatusPresent && resp.Status != ScsiStatus.Good) - { - throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); + isFinal = resp.Header.FinalPdu; } + else if (pdu.OpCode == OpCode.ScsiDataIn) + { + var resp = ParseResponse(pdu); - isFinal = resp.Header.FinalPdu; - } - else if (pdu.OpCode == OpCode.ScsiDataIn) - { - var resp = ParseResponse(pdu); + // RFC 3720 Section 10.7.4: Validate DataSN sequence + if (resp.DataSequenceNumber != expectedDataSN) + { + throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}"); + } + expectedDataSN++; - // RFC 3720 Section 10.7.4: Validate DataSN sequence - if (resp.DataSequenceNumber != expectedDataSN) - { - throw new InvalidProtocolException($"DataSN mismatch: received {resp.DataSequenceNumber}, expected {expectedDataSN}"); - } - expectedDataSN++; + if (resp.StatusPresent && resp.Status != ScsiStatus.Good) + { + throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); + } - if (resp.StatusPresent && resp.Status != ScsiStatus.Good) - { - throw new ScsiCommandException(resp.Status, "Target indicated SCSI failure"); - } + if (resp.ReadData != null) + { + resp.ReadData.CopyTo(inBuffer.Slice((int)resp.BufferOffset)); + numRead += resp.ReadData.Length; + } - if (resp.ReadData != null) - { - resp.ReadData.CopyTo(inBuffer.Slice((int)resp.BufferOffset)); - numRead += resp.ReadData.Length; + isFinal = resp.Header.FinalPdu; } - - isFinal = resp.Header.FinalPdu; } - } - return Math.Max(numRead, numSent); - } - finally - { - Session.NextTaskTag(); - Session.NextCommandSequenceNumber(); - } + return Math.Max(numRead, numSent); + } + finally + { + Session.NextTaskTag(); + Session.NextCommandSequenceNumber(); + } - retry: - { + retry: + { + } } } - } finally { _streamSemaphore.Release();