fix: handle pipelined SCSI commands during Data-Out collection

The Linux kernel iSCSI initiator pipelines multiple SCSI commands on
the same TCP connection (command queuing). When a write needs R2T for
data beyond the immediate portion, collectDataOut may read a pipelined
SCSI command instead of the expected Data-Out PDU.

Fix: queue non-Data-Out PDUs received during collectDataOut into a
pending buffer. The main dispatch loop drains pending PDUs before
reading from the connection. This correctly handles interleaved
commands during multi-PDU write transfers.

Bug found during WSL2 smoke test: mkfs.ext4 hangs at "Writing
superblocks" because inode table zeroing sends large writes that
exceed FirstBurstLength, triggering R2T while the kernel has already
queued the next command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ping Qiu
2026-02-28 10:08:32 -08:00
parent 6546c549eb
commit bd73a81e00

View File

@@ -48,6 +48,11 @@ type Session struct {
// Data sequencing
dataInWriter *DataInWriter
// PDU queue for commands received during Data-Out collection.
// The initiator pipelines commands; we may read a SCSI Command
// while waiting for Data-Out PDUs.
pending []*PDU
// Shutdown
closed atomic.Bool
closeErr error
@@ -80,7 +85,7 @@ func (s *Session) HandleConnection() error {
defer s.close()
for !s.closed.Load() {
pdu, err := ReadPDU(s.conn)
pdu, err := s.nextPDU()
if err != nil {
if s.closed.Load() {
return nil
@@ -101,6 +106,17 @@ func (s *Session) HandleConnection() error {
return nil
}
// nextPDU returns the next PDU to process, draining the pending queue
// (populated during Data-Out collection) before reading from the connection.
func (s *Session) nextPDU() (*PDU, error) {
if len(s.pending) > 0 {
pdu := s.pending[0]
s.pending = s.pending[1:]
return pdu, nil
}
return ReadPDU(s.conn)
}
// Close terminates the session.
func (s *Session) Close() error {
s.closed.Store(true)
@@ -274,14 +290,17 @@ func (s *Session) collectDataOut(collector *DataOutCollector, itt uint32) error
}
r2tSN++
// Read Data-Out PDUs until F-bit
// Read Data-Out PDUs until F-bit.
// The initiator may pipeline other commands; queue them for later.
for {
doPDU, err := ReadPDU(s.conn)
if err != nil {
return err
}
if doPDU.Opcode() != OpSCSIDataOut {
return fmt.Errorf("expected Data-Out, got %s", OpcodeName(doPDU.Opcode()))
// Not our Data-Out — queue for later dispatch
s.pending = append(s.pending, doPDU)
continue
}
if err := collector.AddDataOut(doPDU); err != nil {
return err