You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by sr...@apache.org on 2026/10/12 07:30:17 UTC

(plc4x) branch develop updated: fix(plc4go): guard serial TransportInstance.Write against concurrent

This is an automated email from the ASF dual-hosted git repository.

sruehl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git


The following commit(s) were added to refs/heads/develop by this push:
     new 2b1095c45d fix(plc4go): guard serial TransportInstance.Write against concurrent
2b1095c45d is described below

commit 2b1095c45dbce86ff61cc7e7ab0763ff9204cec8
Author: Sebastian Rühl <sr...@apache.org>
AuthorDate: Mon Oct 12 09:28:15 2026 +0200

    fix(plc4go): guard serial TransportInstance.Write against concurrent
    
    Close nils serialPort/armer/pacer under stateChangeMutex, but Write and
    SetReadDeadline read those fields unguarded: a data race, and a nil-pointer
    panic when Close lands between Write's nil check and its use of the port.
    
    Snapshot the fields under the lock (not held across the blocking write);
    a write racing Close now fails on the closed port instead.
    
    Closes #2802
---
 plc4go/spi/transports/serial/TransportInstance.go  | 20 ++++++++++----
 .../transports/serial/TransportInstance_test.go    | 32 ++++++++++++++++++++++
 2 files changed, 46 insertions(+), 6 deletions(-)

diff --git a/plc4go/spi/transports/serial/TransportInstance.go b/plc4go/spi/transports/serial/TransportInstance.go
index f99fc91284..c100035b1a 100644
--- a/plc4go/spi/transports/serial/TransportInstance.go
+++ b/plc4go/spi/transports/serial/TransportInstance.go
@@ -172,10 +172,16 @@ func (m *TransportInstance) IsConnected() bool {
 }
 
 func (m *TransportInstance) Write(ctx context.Context, data []byte) error {
-	if !m.connected.Load() {
+	// Snapshot the port and pacer under the state lock: Close nils both, and
+	// a concurrent Close must not race the write (it fails on the closed
+	// port instead). The lock is not held across the blocking write.
+	m.stateChangeMutex.Lock()
+	connected, serialPort, writePacer := m.connected.Load(), m.serialPort, m.pacer
+	m.stateChangeMutex.Unlock()
+	if !connected {
 		return errors.New("error writing to transport. Not connected")
 	}
-	if m.serialPort == nil {
+	if serialPort == nil {
 		return errors.New("error writing to transport. No writer available")
 	}
 	var deadline time.Time
@@ -184,14 +190,14 @@ func (m *TransportInstance) Write(ctx context.Context, data []byte) error {
 	} else if m.cfg.writeTimeout > 0 {
 		deadline = time.Now().Add(m.cfg.writeTimeout)
 	}
-	if err := m.pacer.waitTurn(deadline); err != nil {
+	if err := writePacer.waitTurn(deadline); err != nil {
 		return errors.Wrap(err, "error pacing write")
 	}
-	if err := m.serialPort.SetWriteDeadline(deadline); err != nil {
+	if err := serialPort.SetWriteDeadline(deadline); err != nil {
 		return errors.Wrap(err, "error setting write deadline")
 	}
-	num, err := m.serialPort.Write(data)
-	m.pacer.noteActivity()
+	num, err := serialPort.Write(data)
+	writePacer.noteActivity()
 	if err != nil {
 		return errors.Wrap(err, "error writing")
 	}
@@ -206,7 +212,9 @@ func (m *TransportInstance) GetReader() transports.ExtendedReader {
 }
 
 func (m *TransportInstance) SetReadDeadline(deadline time.Time) error {
+	m.stateChangeMutex.Lock()
 	armer := m.armer
+	m.stateChangeMutex.Unlock()
 	if armer == nil {
 		return errors.New("error setting read deadline. Not connected")
 	}
diff --git a/plc4go/spi/transports/serial/TransportInstance_test.go b/plc4go/spi/transports/serial/TransportInstance_test.go
index 0ecab2bdbe..740f35ea99 100644
--- a/plc4go/spi/transports/serial/TransportInstance_test.go
+++ b/plc4go/spi/transports/serial/TransportInstance_test.go
@@ -388,3 +388,35 @@ func TestWrite_DedicatedInterframePacing(t *testing.T) {
 	require.NoError(t, m.Write(context.Background(), []byte{0x02}))
 	assert.GreaterOrEqual(t, time.Since(start), 40*time.Millisecond, "second write must wait out the gap")
 }
+
+// Close nils serialPort/armer/pacer under stateChangeMutex; Write and
+// SetReadDeadline must not read those fields unguarded (GH-2802). Run under
+// -race: the detector flags the unguarded read, and without it a Close landing
+// between Write's nil check and its use of the port panics.
+func TestTransportInstance_WriteAndSetReadDeadlineRaceClose(t *testing.T) {
+	for range 50 {
+		m := NewTransportInstanceWithConfig("test", func() serialConfig {
+			c := defaultSerialConfig()
+			c.interframeDelay = time.Microsecond
+			return c
+		}(), NewTransport())
+		m.serialPort = stubPort{}
+		m.pacer = newPacer(m.cfg.interframeDelay)
+		m.armer = newDeadlineReader(m.serialPort, m.cfg.readTimeout, m.pacer)
+		m.connected.Store(true)
+
+		done := make(chan struct{})
+		go func() {
+			defer close(done)
+			for range 100 {
+				_ = m.Write(context.Background(), []byte{0x01})
+				_ = m.SetReadDeadline(time.Time{})
+			}
+		}()
+		require.NoError(t, m.Close())
+		<-done
+
+		assert.Error(t, m.Write(context.Background(), []byte{0x01}), "write after close must fail")
+		assert.Error(t, m.SetReadDeadline(time.Time{}), "set read deadline after close must fail")
+	}
+}