Summary
github.com/hashicorp/yamux v0.1.2 Stream.Read (and Stream.write) can spin a single goroutine at 100% CPU forever when the underlying session is shut down while a read/write is blocked and the stream is still in streamEstablished.
In each WAIT block the select has:
select {
case <-s.session.shutdownCh: // closed on session teardown — no body, no return
case <-s.recvNotifyCh:
case <-timeout:
return 0, ErrTimeout
}
...
goto START
shutdownCh is a closed channel after teardown, so once the session is gone that case is always ready. It has no body, so it falls through to goto START. START only inspects the per-stream s.state, which stays streamEstablished when a session is torn down out from under an in-flight read — so START finds no data and jumps back to WAIT, which re-fires shutdownCh immediately, re-allocating a time.NewTimer each pass. Result: one goroutine pegs a core at ~370k time.NewTimer/s and never returns.
Where it bites us
The wasm (js/wasm) hypervisor build is single-threaded, so a single spinning Stream.Read starves the entire runtime — the tab pegs a CPU core and stops responding. It reproduces when a dmsg WS / WebRTC carrier drops during an in-flight dial (session teardown mid-readResponse). Native builds are far less exposed (multi-core, TCP carriers rarely drop mid-read) but the same latent spin exists.
Fix
Return from the shutdownCh case in both Read and write instead of falling through to goto START:
case <-s.session.shutdownCh:
return 0, ErrSessionShutdown
A shut-down session can neither deliver more data nor drain the send window, so returning is strictly correct.
Status in this repo
Applied locally by copying yamux in-module under third_party/hashicorp/yamux/ (import path github.com/skycoin/skywire/third_party/hashicorp/yamux, package unchanged) with the fix + PATCHES.md provenance. An upstream PR to hashicorp/yamux should follow (needs a fork); this issue tracks that follow-up so the local copy can eventually be dropped.
Summary
github.com/hashicorp/yamuxv0.1.2Stream.Read(andStream.write) can spin a single goroutine at 100% CPU forever when the underlying session is shut down while a read/write is blocked and the stream is still instreamEstablished.In each
WAITblock the select has:shutdownChis a closed channel after teardown, so once the session is gone that case is always ready. It has no body, so it falls through togoto START.STARTonly inspects the per-streams.state, which staysstreamEstablishedwhen a session is torn down out from under an in-flight read — so START finds no data and jumps back toWAIT, which re-firesshutdownChimmediately, re-allocating atime.NewTimereach pass. Result: one goroutine pegs a core at ~370ktime.NewTimer/s and never returns.Where it bites us
The wasm (
js/wasm) hypervisor build is single-threaded, so a single spinningStream.Readstarves the entire runtime — the tab pegs a CPU core and stops responding. It reproduces when a dmsg WS / WebRTC carrier drops during an in-flight dial (session teardown mid-readResponse). Native builds are far less exposed (multi-core, TCP carriers rarely drop mid-read) but the same latent spin exists.Fix
Return from the
shutdownChcase in bothReadandwriteinstead of falling through togoto START:A shut-down session can neither deliver more data nor drain the send window, so returning is strictly correct.
Status in this repo
Applied locally by copying yamux in-module under
third_party/hashicorp/yamux/(import pathgithub.com/skycoin/skywire/third_party/hashicorp/yamux, package unchanged) with the fix +PATCHES.mdprovenance. An upstream PR to hashicorp/yamux should follow (needs a fork); this issue tracks that follow-up so the local copy can eventually be dropped.