tsnet: fix data race in chanTUN test device

The wireguard-go receive path could be in chanTUN.Write, selecting to
send on the Inbound channel, while test cleanup called chanTUN.Close,
which closed that same channel. The select on the closed channel in
Write did not synchronize with Close, so the race detector flagged
the send racing with the close. It could also have panicked with a
send on a closed channel.

Add a mutex serializing Write and Close. Write now checks for closed
under the lock before doing a non-blocking send, so Close can't close
Inbound mid-send.

Fixes #20541

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: I8d8d10250cef0c1931753c78eebff6e8286f7201
This commit is contained in:
Brad Fitzpatrick
2026-07-20 08:25:22 -07:00
committed by Brad Fitzpatrick
parent 246c82a658
commit 2dd5d82f56
+13 -2
View File
@@ -2560,6 +2560,10 @@ type chanTUN struct {
Outbound chan []byte // packets to read from TUN Outbound chan []byte // packets to read from TUN
closed chan struct{} closed chan struct{}
events chan tun.Event events chan tun.Event
// wmu serializes Write and Close so a Write can't send on
// Inbound while Close is closing it.
wmu sync.Mutex
} }
func newChanTUN() *chanTUN { func newChanTUN() *chanTUN {
@@ -2576,6 +2580,8 @@ func newChanTUN() *chanTUN {
func (t *chanTUN) File() *os.File { panic("not implemented") } func (t *chanTUN) File() *os.File { panic("not implemented") }
func (t *chanTUN) Close() error { func (t *chanTUN) Close() error {
t.wmu.Lock()
defer t.wmu.Unlock()
select { select {
case <-t.closed: case <-t.closed:
default: default:
@@ -2596,14 +2602,19 @@ func (t *chanTUN) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
} }
func (t *chanTUN) Write(bufs [][]byte, offset int) (int, error) { func (t *chanTUN) Write(bufs [][]byte, offset int) (int, error) {
t.wmu.Lock()
defer t.wmu.Unlock()
select {
case <-t.closed:
return 0, errors.New("closed")
default:
}
for _, buf := range bufs { for _, buf := range bufs {
pkt := buf[offset:] pkt := buf[offset:]
if len(pkt) == 0 { if len(pkt) == 0 {
continue continue
} }
select { select {
case <-t.closed:
return 0, errors.New("closed")
case t.Inbound <- slices.Clone(pkt): case t.Inbound <- slices.Clone(pkt):
default: default:
// Drop the packet if the channel is full, like a real // Drop the packet if the channel is full, like a real