DEBUG.WIKI
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
RackNerd;$21.99/年 · 1GB KVM

Go ChaCha8 恢复状态报 slice bounds,失败的 UnmarshalBinary 也可能改动输出

ChaCha8 UnmarshalBinary 的超长读缓冲会 panic,无效核心状态还可能先污染输出;先限制已知格式,再在候选生成器上恢复,成功才替换。

从文件或缓存恢复 math/rand/v2.ChaCha8 状态时,UnmarshalBinary 原本应报告编码错误,却可能直接触发切片越界。还有更隐蔽的一种结果:方法返回错误,后续 Read 已经使用了失败输入中的缓冲字节。

已核对的 Go 1.27.2 源码仍先写入读缓冲,再验证剩余核心状态。若 readbuf: 后的长度超过内部八字节缓冲,计算切片起点时会越界;长度较短但后续核心状态无效时,缓冲也可能先被修改。这两个表现来自同一段“先改对象、后验数据”的恢复路径。

这里讨论标准库生成器的二进制状态,不是普通随机种子,也不是 JSON。不要把任意输入直接当成可信快照,或依靠固定种子掩盖状态文件损坏。

恢复失败后,原对象不能默认继续用

检查调用方是否这样处理:记录 unmarshal 错误,然后继续使用同一个生成器。对受影响实现,这不能保证随机流仍与恢复前一致。

较稳妥的临时边界是:先拒绝已知的超长读缓冲,再把恢复放到独立候选对象上,只在成功后替换原对象。例如下面的 Go 1.27 适配函数:

import (
    "bytes"
    "errors"
    rand "math/rand/v2"
)

func restoreChaCha8(dst *rand.ChaCha8, data []byte) error {
    if dst == nil {
        return errors.New("nil ChaCha8 destination")
    }
    prefix := []byte("readbuf:")
    if bytes.HasPrefix(data, prefix) && len(data) > len(prefix) {
        if data[len(prefix)] > 8 {
            return errors.New("oversized ChaCha8 read buffer")
        }
    }
    next := rand.NewChaCha8([32]byte{})
    if err := next.UnmarshalBinary(data); err != nil {
        return err
    }
    *dst = *next
    return nil
}

它针对已核对编码中的这条缺陷,不是自行定义的新快照格式。长度前缀不完整等其他无效输入仍交给标准库报告错误;成功才替换,失败时保留原生成器。不要去掉前面的长度检查,否则独立候选对象同样会遇到已知 panic。

并发访问仍需由调用者同步;函数没有让 ChaCha8 变成可并发恢复与读取的对象。若恢复本应创建一条全新的业务序列,也可以在失败时直接终止该任务,不必勉强保留旧生成器继续运行。

限制快照来源,升级后复核适配层

输入应来自受控的状态保存流程,保留原始损坏数据用于诊断,不要先覆盖唯一快照。需要完整性保证时,按应用原来的认证或校验机制保护存储;仅检查缓冲长度不能证明快照未被篡改。

截至 2026 年 10 月 21 日,修正为“验证成功后再提交状态”的工作已有代码评审,跟踪目标是 Go 1.28。里程碑和评审都不等于已发布的标准库修复;选择维护版本时核对正式包含情况,并重新构建应用。

回归应包含合法快照往返、读过非八字节整数倍数据后的快照、超长读缓冲、截断输入和无效核心状态。失败路径要同时检查错误与后续输出是否保持原状态,不能只断言“不 panic”。更新工具链后,保留这些样例再判断是否移除临时格式检查。