Go binary.Decode 解码 bool 切片时,输入多一个字节却发生越界
Go 1.27.2 的 bool、int8 快速解码遍历了全部输入,长于目标就可能越界。限定本次字段区间,保留消费数和后续协议数据。
binary.Decode 的目标是四个 bool,输入有五个字节,程序不是返回消耗字节数,而是触发 index out of range。如果目标类型是 []bool 或 []int8,要检查标准库快速解码路径有没有遍历整段输入。
Go 1.27.2 的正式源码仍有这条问题路径。输入足够长的检查通过后,Decode 把原切片交给快速解码器;这两种类型按输入长度逐字节循环,却向更短的目标切片写入。其他目标类型的实现不同,不能由此认定所有 binary.Decode 都会越界。
触发形态可以缩到下面这几行,示例只描述输入与目标的关系:
msg := []byte{1, 0, 1, 0, 99}
flags := make([]bool, 4)
n, err := binary.Decode(msg, binary.LittleEndian, flags)
预期消费前四个字节,最后一个字节仍属于后续数据。这里不能为了避免 panic,就把整个五字节输入判为必须丢弃的坏包;协议有可能允许多个字段共用一个接收缓冲区。
在调用边界限定本次解码的区间
对于已确认的 bool 字段,可以先检查目标需要的大小,保留短输入错误,再只传入本次字段的字节。下面函数只处理 []bool,并且返回实际消费数:
import (
"encoding/binary"
"io"
)
func decodeFlags(buf []byte, dst []bool) (int, error) {
size := len(dst) // encoding/binary uses one byte per bool
if len(buf) < size {
return 0, io.ErrUnexpectedEOF
}
return binary.Decode(buf[:size], binary.LittleEndian, dst)
}
调用成功后,下一字段从 buf[n:] 开始处理,不把尾部数据一起丢掉。零长度目标需要消费零字节;目标长度应由受信协议约束决定,不能直接按不受限外部长度任意分配内存。这里没有让 bool 编码变成位图:binary 中每个 bool 仍占一个字节。
相同思路适用于已确认的 int8 切片,因为每个元素也占一字节;其他固定大小类型可以按 binary.Size 计算,但要先处理返回负值的类型,再检查输入长度。不要把这个专用函数泛化成“任何 Go 对象都能二进制恢复”。
也不建议在网络处理最外层一律 recover 后继续使用已写入一部分的目标。panic 前可能已经修改了若干元素,吞掉异常会让下游读到混合状态。应在明确的解码边界保证输入区间,而不是靠异常恢复猜测消耗位置。
主分支修正已把循环限制到目标长度,相关变更完成不等于 1.27.2 已携带它。选择后续工具链时核对正式版本代码,并重新构建程序;替换机器上的 go 命令不会自动改变已部署二进制。
回归应覆盖空目标、短输入、恰好长度,以及带下一字段的长输入,分别确认错误、消费数、目标内容和未消费尾部。两个 bool 字段连续解析时尤其要检查第二段起点,避免虽然不再 panic,却把后续协议字段错位。


