必须在io.ReadFull前校验pkgLen,否则恶意构造的超大值会导致OOM或永久阻塞;io.ReadFull保证读满字节数,而io.Read可能只读部分导致解析错误。

分包边界溢出不是 binary.Read 的 bug,是协议层校验缺失的必然结果——不校验 pkgLen,攻击者发个 0xFFFFFFFF 就能让服务分配 4GB 内存然后 OOM。
为什么 pkgLen 校验必须在 io.ReadFull 前做
binary.Read 从 net.Conn 读出的 pkgLen 是裸值,没经过任何可信过滤。如果协议头用 uint32 表示包长,但没限制范围,恶意客户端可构造任意大值:
- 不校验直接
make([]byte, pkgLen)→ 触发系统内存耗尽或 Go runtime panic: "runtime: out of memory" - 即使机器扛住分配,后续
io.ReadFull(r, data)会永久阻塞(对方不发满就卡死)或返回io.ErrUnexpectedEOF - 校验必须在分配前:比如
if pkgLen == 0 || pkgLen > 1024*1024 { return errors.New("invalid package length") }
io.ReadFull 和 io.Read 在分包时的区别
分包阶段唯一能接受的读取方式是 io.ReadFull;用 io.Read 等于放弃完整性保障:
-
io.Read(data)可能只读到前 12 字节(比如刚好一个 TCP segment),data剩余部分保持零值 → 后续binary.Read(bytes.NewReader(data), ...)解析出全错数值 -
io.ReadFull(data)保证要么读满len(data)字节,要么返回错误(io.ErrUnexpectedEOF或连接断开),无中间态 - 注意:传给
io.ReadFull的必须是切片(如buf[:pkgLen]),不能是底层数组;否则可能 panic: "slice bounds out of range"
如何安全处理超长包导致的粘包错位
当 pkgLen 校验失败(比如超限)时,不能简单关闭连接——残留字节还在 socket 缓冲区里,下次读会从错位位置开始,导致后续所有包解析失败:
立即学习“go语言免费学习笔记(深入)”;
- 先调
conn.SetReadDeadline(time.Now().Add(1 * time.Second))防卡死 - 用循环 +
io.Read丢弃最多pkgLen字节(但不超过预设上限,如 64KB),直到读到 EOF 或超时 - 丢弃后立即重置 deadline:
conn.SetReadDeadline(time.Time{}) - 若丢弃过程中遇到非临时错误(如
io.EOF、net.OpError),才真正断连
复用缓冲区时 pkgLen 溢出引发的隐性越界
用 sync.Pool 复用 []byte 能降 GC 压力,但若复用的缓冲区长度小于实际 pkgLen,会导致静默截断或 panic:
- 从 pool 获取后必须检查:
if len(buf) - 写入前确保容量足够:
if cap(buf) ,避免 <code>append触发新分配 - 归还 pool 前清理长度:
buf = buf[:0],防止下次取出时残留脏数据
最危险的点不在解析逻辑本身,而在「校验之后、分配之前」这个毫秒级窗口——这里没加锁、没 deadline、没防御性拷贝,一次疏忽就会让整个连接上下文错位,且难以复现。



















