diff --git a/serial_bsd.go b/serial_bsd.go index f9a7046..4ff04b6 100644 --- a/serial_bsd.go +++ b/serial_bsd.go @@ -11,5 +11,7 @@ package serial import "golang.org/x/sys/unix" func (port *unixPort) Drain() error { - return unix.IoctlSetInt(port.handle, unix.TIOCDRAIN, 0) + return port.withGuard(func() error { + return unix.IoctlSetInt(port.handle, unix.TIOCDRAIN, 0) + }) } diff --git a/serial_darwin.go b/serial_darwin.go index a61ed59..d2de0b3 100644 --- a/serial_darwin.go +++ b/serial_darwin.go @@ -38,9 +38,13 @@ func (port *unixPort) setSpecialBaudrate(speed uint32) error { } func (port *unixPort) ResetInputBuffer() error { - return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) + return port.withGuard(func() error { + return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) + }) } func (port *unixPort) ResetOutputBuffer() error { - return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) + return port.withGuard(func() error { + return unix.IoctlSetPointerInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) + }) } diff --git a/serial_linux.go b/serial_linux.go index b43b9ff..978f9ed 100644 --- a/serial_linux.go +++ b/serial_linux.go @@ -92,8 +92,10 @@ func setTermSettingsBaudrate(speed int, settings *unix.Termios) (error, bool) { } func (port *unixPort) Drain() error { - // It's not super well documented, but this is the same as calling tcdrain: - // - https://git.musl-libc.org/cgit/musl/tree/src/termios/tcdrain.c - // - https://elixir.bootlin.com/linux/v6.2.8/source/drivers/tty/tty_io.c#L2673 - return unix.IoctlSetInt(port.handle, unix.TCSBRK, 1) + return port.withGuard(func() error { + // It's not super well documented, but this is the same as calling tcdrain: + // - https://git.musl-libc.org/cgit/musl/tree/src/termios/tcdrain.c + // - https://elixir.bootlin.com/linux/v6.2.8/source/drivers/tty/tty_io.c#L2673 + return unix.IoctlSetInt(port.handle, unix.TCSBRK, 1) + }) } diff --git a/serial_postclose_linux_test.go b/serial_postclose_linux_test.go new file mode 100644 index 0000000..52dccec --- /dev/null +++ b/serial_postclose_linux_test.go @@ -0,0 +1,77 @@ +// +// Copyright 2014-2026 Cristian Maglie. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +//go:build linux + +package serial + +import ( + "errors" + "fmt" + "os" + "testing" + "time" + + "golang.org/x/sys/unix" +) + +// openPTYPort opens a Linux pseudo-terminal and returns a serial Port bound to +// its slave side. No real serial hardware is required. +func openPTYPort(t *testing.T) Port { + t.Helper() + master, err := os.OpenFile("/dev/ptmx", os.O_RDWR|unix.O_NOCTTY, 0) + if err != nil { + t.Skipf("cannot open /dev/ptmx: %v", err) + } + t.Cleanup(func() { _ = master.Close() }) + mfd := int(master.Fd()) + if err := unix.IoctlSetPointerInt(mfd, unix.TIOCSPTLCK, 0); err != nil { + t.Skipf("TIOCSPTLCK: %v", err) + } + n, err := unix.IoctlGetInt(mfd, unix.TIOCGPTN) + if err != nil { + t.Skipf("TIOCGPTN: %v", err) + } + slave := fmt.Sprintf("/dev/pts/%d", n) + port, err := Open(slave, &Mode{BaudRate: 9600}) + if err != nil { + t.Skipf("cannot open pty slave %s: %v", slave, err) + } + return port +} + +// TestOperationsAfterCloseReturnPortClosed verifies that every method that +// touches the file descriptor returns PortClosed after the port is closed, +// instead of operating on a stale fd whose number the OS may have reassigned. +func TestOperationsAfterCloseReturnPortClosed(t *testing.T) { + port := openPTYPort(t) + if err := port.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + assertClosed := func(name string, err error) { + t.Helper() + var pe *PortError + if !errors.As(err, &pe) || pe.Code() != PortClosed { + t.Errorf("%s after Close: got %v, want PortError{PortClosed}", name, err) + } + } + + n, err := port.Write([]byte("data")) + if n != 0 { + t.Errorf("Write after Close: wrote %d bytes, want 0", n) + } + assertClosed("Write", err) + assertClosed("Drain", port.Drain()) + assertClosed("ResetInputBuffer", port.ResetInputBuffer()) + assertClosed("ResetOutputBuffer", port.ResetOutputBuffer()) + assertClosed("SetMode", port.SetMode(&Mode{BaudRate: 9600})) + assertClosed("SetDTR", port.SetDTR(true)) + assertClosed("SetRTS", port.SetRTS(false)) + assertClosed("Break", port.Break(time.Millisecond)) + _, err = port.GetModemStatusBits() + assertClosed("GetModemStatusBits", err) +} diff --git a/serial_resetbuf_linux_bsd.go b/serial_resetbuf_linux_bsd.go index b2b7dca..0a3ed39 100644 --- a/serial_resetbuf_linux_bsd.go +++ b/serial_resetbuf_linux_bsd.go @@ -11,9 +11,13 @@ package serial import "golang.org/x/sys/unix" func (port *unixPort) ResetInputBuffer() error { - return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) + return port.withGuard(func() error { + return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCIFLUSH) + }) } func (port *unixPort) ResetOutputBuffer() error { - return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) + return port.withGuard(func() error { + return unix.IoctlSetInt(port.handle, ioctlTcflsh, unix.TCOFLUSH) + }) } diff --git a/serial_unix.go b/serial_unix.go index 88de16b..68a02de 100644 --- a/serial_unix.go +++ b/serial_unix.go @@ -109,7 +109,26 @@ func (port *unixPort) Read(p []byte) (int, error) { } } +// withGuard runs op while the port is held open, mirroring the guard used by +// Read: it takes the close-lock read side and verifies the port is still open +// before op touches the file descriptor. Without it, a method called after (or +// concurrently with) Close would operate on a closed fd whose number the OS may +// already have reassigned to another file. Returns PortClosed when closed. +func (port *unixPort) withGuard(op func() error) error { + port.closeLock.RLock() + defer port.closeLock.RUnlock() + if atomic.LoadUint32(&port.opened) != 1 { + return &PortError{code: PortClosed} + } + return op() +} + func (port *unixPort) Write(p []byte) (n int, err error) { + port.closeLock.RLock() + defer port.closeLock.RUnlock() + if atomic.LoadUint32(&port.opened) != 1 { + return 0, &PortError{code: PortClosed} + } n, err = unix.Write(port.handle, p) if n < 0 { // Do not return -1 unix errors n = 0 @@ -118,76 +137,84 @@ func (port *unixPort) Write(p []byte) (n int, err error) { } func (port *unixPort) Break(t time.Duration) error { - if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil { - return err - } + return port.withGuard(func() error { + if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil { + return err + } - time.Sleep(t) + time.Sleep(t) - if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil { - return err - } + if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil { + return err + } - return nil + return nil + }) } func (port *unixPort) SetMode(mode *Mode) error { - settings, err := port.getTermSettings() - if err != nil { - return err - } - if err := setTermSettingsParity(mode.Parity, settings); err != nil { - return err - } - if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil { - return err - } - if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil { - return err - } - requireSpecialBaudrate := false - if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil { - return err - } else if special { - requireSpecialBaudrate = true - } - if err := port.setTermSettings(settings); err != nil { - return err - } - if requireSpecialBaudrate { - // MacOSX require this one to be the last operation otherwise an - // 'Invalid serial port' error is produced. - if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil { + return port.withGuard(func() error { + settings, err := port.getTermSettings() + if err != nil { return err } - } - return nil + if err := setTermSettingsParity(mode.Parity, settings); err != nil { + return err + } + if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil { + return err + } + if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil { + return err + } + requireSpecialBaudrate := false + if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil { + return err + } else if special { + requireSpecialBaudrate = true + } + if err := port.setTermSettings(settings); err != nil { + return err + } + if requireSpecialBaudrate { + // MacOSX require this one to be the last operation otherwise an + // 'Invalid serial port' error is produced. + if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil { + return err + } + } + return nil + }) } func (port *unixPort) SetDTR(dtr bool) error { - status, err := port.getModemBitsStatus() - if err != nil { - return err - } - if dtr { - status |= unix.TIOCM_DTR - } else { - status &^= unix.TIOCM_DTR - } - return port.setModemBitsStatus(status) + return port.withGuard(func() error { + status, err := port.getModemBitsStatus() + if err != nil { + return err + } + if dtr { + status |= unix.TIOCM_DTR + } else { + status &^= unix.TIOCM_DTR + } + return port.setModemBitsStatus(status) + }) } func (port *unixPort) SetRTS(rts bool) error { - status, err := port.getModemBitsStatus() - if err != nil { - return err - } - if rts { - status |= unix.TIOCM_RTS - } else { - status &^= unix.TIOCM_RTS - } - return port.setModemBitsStatus(status) + return port.withGuard(func() error { + status, err := port.getModemBitsStatus() + if err != nil { + return err + } + if rts { + status |= unix.TIOCM_RTS + } else { + status &^= unix.TIOCM_RTS + } + return port.setModemBitsStatus(status) + }) } func (port *unixPort) SetReadTimeout(timeout time.Duration) error { @@ -199,16 +226,21 @@ func (port *unixPort) SetReadTimeout(timeout time.Duration) error { } func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) { - status, err := port.getModemBitsStatus() - if err != nil { - return nil, err - } - return &ModemStatusBits{ - CTS: (status & unix.TIOCM_CTS) != 0, - DCD: (status & unix.TIOCM_CD) != 0, - DSR: (status & unix.TIOCM_DSR) != 0, - RI: (status & unix.TIOCM_RI) != 0, - }, nil + var bits *ModemStatusBits + err := port.withGuard(func() error { + status, err := port.getModemBitsStatus() + if err != nil { + return err + } + bits = &ModemStatusBits{ + CTS: (status & unix.TIOCM_CTS) != 0, + DCD: (status & unix.TIOCM_CD) != 0, + DSR: (status & unix.TIOCM_DSR) != 0, + RI: (status & unix.TIOCM_RI) != 0, + } + return nil + }) + return bits, err } func nativeOpen(portName string, mode *Mode) (*unixPort, error) {