Skip to content

fix(reader): stop TimeoutReader writing to p after timing out - #847

Open
dwisiswant0 wants to merge 1 commit into
mainfrom
dwisiswant0/fix/reader/stop-timeoutreader-writing-to-p-after-timing-out
Open

dwisiswant0 wants to merge 1 commit into
mainfrom
dwisiswant0/fix/reader/stop-timeoutreader-writing-to-p-after-timing-out

Conversation

@dwisiswant0

Copy link
Copy Markdown
Member

TimeoutReader.Read handed the caller's buffer to a helper goroutine and
returned ErrTimeout without waiting for it. When the underlying Read
finished later, the goroutine wrote into p, which the caller may have
reused by then, and into the named results n and err, a data race. It
then blocked forever sending on an unbuffered channel nobody received
from, leaking the goroutine. With a zero Timeout the context was never
created and ctx.Done() dereferenced a nil interface, although the
original implementation documented zero as "behaves like a normal
reader".

io.Reader cannot cancel a blocked Read, so the helper still runs until
the source returns. Read now gives the source a private scratch buffer,
copies into p only when the result arrives before the timer fires, and
passes the result over a buffered channel so the helper exits as soon as
the source returns. A Timeout of zero or less reads directly from the
source. The context and the per-value datachan field are gone; the
unexported field was never shared across calls because Read has a value
receiver.

Bytes delivered after a timeout are dropped, since a value receiver and
exported literal construction leave nowhere to keep them; the doc
comment says so. The scratch buffer costs an allocation and a copy per
Read, each as long as the caller's buffer, which is the price of never
sharing that buffer with the helper.

Fixes: #837

TimeoutReader.Read handed the caller's buffer to a helper goroutine and
returned ErrTimeout without waiting for it. When the underlying Read
finished later, the goroutine wrote into p, which the caller may have
reused by then, and into the named results n and err, a data race. It
then blocked forever sending on an unbuffered channel nobody received
from, leaking the goroutine. With a zero Timeout the context was never
created and ctx.Done() dereferenced a nil interface, although the
original implementation documented zero as "behaves like a normal
reader".

io.Reader cannot cancel a blocked Read, so the helper still runs until
the source returns. Read now gives the source a private scratch buffer,
copies into p only when the result arrives before the timer fires, and
passes the result over a buffered channel so the helper exits as soon as
the source returns. A Timeout of zero or less reads directly from the
source. The context and the per-value datachan field are gone; the
unexported field was never shared across calls because Read has a value
receiver.

Bytes delivered after a timeout are dropped, since a value receiver and
exported literal construction leave nowhere to keep them; the doc
comment says so. The scratch buffer costs an allocation and a copy per
Read, each as long as the caller's buffer, which is the price of never
sharing that buffer with the helper.

Fixes: #837
Signed-off-by: Dwi Siswanto <git@dw1.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reader: TimeoutReader writes into the caller's buffer after returning ErrTimeout

1 participant