mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
GHSA-jg62-j5h6-8mpq: the terminal and file-manager endpoints created unbounded IO streams; an authenticated member could open thousands, each spawning goroutines, a 1MiB buffer and an agent-side PTY, exhausting dashboard and agent resources. CreateStream now enforces a per-user (20) and per-server (40) cap in the existing ioStreamMutex critical section, using the stream map as the single source of truth. Dashboard-internal streams (uid==0: NAT, server transfer, MCP transfer) skip the per-user cap but still count per-server. Adds caps, exemption, slot-release and no-leak regression tests.
68 lines
1.9 KiB
Go
68 lines
1.9 KiB
Go
package rpc
|
|
|
|
import (
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// settleGoroutines lets transient goroutines wind down so the count reflects
|
|
// only durable leaks, not in-flight teardown.
|
|
func settleGoroutines() int {
|
|
var n int
|
|
for i := 0; i < 50; i++ {
|
|
runtime.GC()
|
|
time.Sleep(20 * time.Millisecond)
|
|
n = runtime.NumGoroutine()
|
|
}
|
|
return n
|
|
}
|
|
|
|
// TestStartStream_NoGoroutineLeakAfterClose verifies the bidirectional relay in
|
|
// StartStream does not strand a goroutine. StartStream launches two
|
|
// io.CopyBuffer goroutines (user<-agent and agent<-user) but returns after the
|
|
// first one finishes. The second goroutine stays blocked in CopyBuffer until
|
|
// its endpoints are closed. CloseStream closes both endpoints, which must
|
|
// unblock and drain that second goroutine. If it doesn't, every terminal / fm /
|
|
// NAT session leaks one goroutine for the lifetime of the dashboard.
|
|
func TestStartStream_NoGoroutineLeakAfterClose(t *testing.T) {
|
|
base := settleGoroutines()
|
|
|
|
const n = 20
|
|
for i := 0; i < n; i++ {
|
|
h := NewNezhaHandler()
|
|
const id = "leak-stream"
|
|
|
|
if err := h.CreateStream(id, 1, 1); err != nil {
|
|
t.Fatalf("CreateStream: %v", err)
|
|
}
|
|
|
|
userIo, agentIo := newPipeReadWriter(), newPipeReadWriter()
|
|
h.AgentConnected(id, agentIo)
|
|
h.UserConnected(id, userIo)
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_ = h.StartStream(id, time.Second*5)
|
|
close(done)
|
|
}()
|
|
|
|
// Close one endpoint so the first CopyBuffer returns and StartStream
|
|
// unblocks, mirroring a peer disconnect.
|
|
time.Sleep(10 * time.Millisecond)
|
|
userIo.Close()
|
|
<-done
|
|
|
|
// The caller's defer CloseStream closes both endpoints, which must
|
|
// drain the still-blocked second copy goroutine.
|
|
_ = h.CloseStream(id)
|
|
agentIo.Close()
|
|
}
|
|
|
|
after := settleGoroutines()
|
|
if grew := after - base; grew > 2 {
|
|
t.Fatalf("goroutine leak in StartStream relay: ran %d streams, goroutines grew by %d (base=%d after=%d)",
|
|
n, grew, base, after)
|
|
}
|
|
}
|