Skip to content

vt: data race on Emulator.closed between Read and Close #879

Description

@LXXero

Emulator.Read and Emulator.Close both touch the unexported closed bool with no synchronization, and SafeEmulator doesn't protect it either — so calling Close() to unblock a goroutine parked in Read() is a data race.

Emulator.Read reads closed, then blocks on the pipe (vt/emulator.go:251):

func (e *Emulator) Read(p []byte) (n int, err error) {
	if e.closed {        // unsynchronized read
		return 0, io.EOF
	}
	return e.pr.Read(p)  // blocks here
}

Emulator.Close writes it (vt/emulator.go:260):

func (e *Emulator) Close() error {
	if e.closed { return nil }   // unsynchronized read
	e.closed = true              // unsynchronized write
	return e.pw.CloseWithError(io.EOF)
}

SafeEmulator.Read deliberately takes no lock (so it can block without holding the mutex across the read), and there's no SafeEmulator.Close override, so the embedded Emulator.Close also runs unlocked (vt/safe_emulator.go:33).

The underlying io.Pipe is already concurrency-safe — pw.CloseWithError(io.EOF) correctly unblocks a blocked pr.Read. The only unsafe shared state is the closed flag.

Impact

The natural teardown pattern — one goroutine draining Read(), another calling Close() to unblock it — trips go test -race with a data race on closed (read at emulator.go:252, write at emulator.go:265). Consumers are forced to choose between leaking the Read goroutine or accepting the race.

Suggested fix

Make closed an atomic.Bool (load in Read/Write, Swap in Close) — or drop the closed short-circuit entirely and rely on the pipe (a closed pipe already makes pr.Read return the close error and pw.Write return ErrClosedPipe). Either way no mutex needs to be held across the blocking read.

Observed on vt module version v0.0.0-20260323091123-df7b1bcffcca.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions