mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-22 03:00:12 +00:00
fix(rpc): cap concurrent IO streams per user and per server
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.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user