fix: DetectPostHandshakeRecordsLens background probe bugs (panic, leak, race) - #36
fix: DetectPostHandshakeRecordsLens background probe bugs (panic, leak, race)#36hexonal wants to merge 2 commits into
Conversation
Three bugs found while running this in production (invoked unconditionally
from NewListener() for every REALITY inbound):
1. PostHandshakeRecordDetectConn.Read() did `data = data[length:]` with no
bounds check. A truncated final record from the disguise dest (network
cut/RST/read-deadline firing mid-record) can legitimately report a
length longer than what actually arrived - io.ReadAll's error is
discarded, so `data` may be a partial read. Without the check,
`data[length:]` panics with "slice bounds out of range", and since this
runs unrecovered in a background goroutine spawned by NewListener, it
takes down the entire process - on every single REALITY inbound, not
just this calibration probe. Fixed with a length-vs-len(data) check
before slicing.
2. Neither background probe goroutine recovers from a panic (this one or
any other). Since NewListener spawns them with no caller to propagate
an error to, an unrecovered panic here is a process-wide crash triggered
by nothing more than normal TLS behavior from the disguise dest. Added
`defer func() { recover() }()` to both.
3. Smaller issues found while reviewing the same function:
- The two `net.Dial`'d probe connections were never Close()'d - a
bounded (one per unique dest/SNI/alpn key, gated by the existing
LoadOrStore) but real leak of a goroutine+socket on every REALITY
listener startup. Added `defer target.Close()` to both.
- CCSDetectConn.Write's background reader goroutine assigned into
`err` - Write's own named return value - racing unsynchronized
against every `return c.Conn.Write(b)` in the same method (flagged by
`go test -race`). Switched to a local variable; nothing here needs to
surface the read error to the caller.
Verified XTLS#1 and XTLS#2 with a standalone repro (not included in this diff):
a net.Pipe()-backed PostHandshakeRecordDetectConn fed a truncated final
record panics on the unpatched code and returns a clean EOF after the fix.
|
@RPRX 一个月了还没人看过,冒昧顶一下。 先说清楚这不是理论推演,是能复现的:用 具体路径: 关键在于它跑在哪: 顺带修的两个小问题也在同一个函数里:两个 我知道 #34 那种"看起来可能会 panic"的推测性修复你关掉是对的。这个不一样 —— 有确定的触发路径和可复现的 repro。如果你觉得 recover 那部分做法不合适,我可以只留边界检查那一条。 |
|
一股AI味。。。 |
Summary
DetectPostHandshakeRecordsLens(record_detect.go) is invoked unconditionally fromNewListener()for every REALITY inbound, spawning two background calibration probes per unique(dest, serverName, alpn)key against the disguise destination. Found and fixed three bugs while running this in production:1. Slice-bounds panic (crashes the whole process)
PostHandshakeRecordDetectConn.Read()does:with no check that
length <= len(data). A truncated final record from the disguise dest (network cut/RST/read-deadline firing mid-record) can legitimately report a length longer than what actually arrived —io.ReadAll's error is discarded a few lines up, sodatamay be a partial read. Without a bounds check,data[length:]panics withslice bounds out of range. Since this runs unrecovered in a background goroutine spawned byNewListener(), this takes down the entire process — reachable on every single REALITY inbound listener, not just this one calibration probe, purely from ordinary network conditions against the disguise site (nothing adversarial required).Fix: bounds-check
lengthagainstlen(data)before slicing.2. No panic recovery in either probe goroutine
Neither background goroutine in
DetectPostHandshakeRecordsLensrecovers from a panic (this one or any other). SinceNewListenerspawns them with no caller able to observe or handle an error, an unrecovered panic here is a process-wide crash triggered by nothing more than unusual-but-legitimate TLS behavior from the disguise destination.Fix: added
defer func() { recover() }()to both goroutines — this is a best-effort calibration probe with no caller to report to, so swallowing a panic here is the correct failure mode (same as the existing pattern of silently returning on anynet.Dial/handshake error a few lines above).3. Two smaller issues found while reviewing the same function
net.Dial'd probe connections (target) were neverClose()'d — a bounded (one per unique key, gated by the existingLoadOrStore) but real leak of a goroutine+socket on every REALITY listener startup. Addeddefer target.Close()to both.CCSDetectConn.Write's background reader goroutine does_, err = c.Conn.Read(buf), assigning intoerr—Write's own named return value — which races unsynchronized against everyreturn c.Conn.Write(b)in the same method (go test -raceflags this). Switched to a localreadErrvariable; nothing here needs to surface the read error to the caller.Testing
Verified #1 and #2 with a standalone repro (a
net.Pipe()-backedPostHandshakeRecordDetectConnfed a truncated final record): panics on the unpatched code, returns a cleanEOFafter the fix.go build ./...passes.