行式输入安全(INV-LINE-01)
bufio.Scanner 超限时 Scan() 返回 false——和 EOF 同一个返回值。本文档描述引擎如何防止这类静默截断。
核心问题
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
process(line)
}
// scanner.Scan() 返回 false:是 EOF?还是一行太长?
// 答案:两者返回同一个值。循环正常退出、函数返回 nil。
默认上限 64KB 不是谁决定的,它只是不写 .Buffer() 时手边的那个值。
历史缺陷(同一形状犯了三次)
| 场景 | 后果 |
|---|---|
| Cell 导入丢 64KB 以上的行 | 数据丢失 |
邮件头解析停在 Auto-Submitted: 之前 | 安全绕过(防环机制被长 References: 头绕过) |
pip list 停在首个长行 | 能力误判(低报已安装包) |
解决方案
内存中的输入:bytes.Lines
// 零拷贝、无上限
for line := range bytes.Lines(data) {
process(line)
}
流式输入:internal/infra/lineio
// 超限报告而非结束流,下一个换行处重新同步
reader := lineio.NewReader(r, maxLineBytes)
for {
line, truncated, err := reader.ReadLine()
if err == io.EOF { break }
if truncated {
// 显式处理截断
}
process(line)
}
网络 body:显式 Buffer
scanner := bufio.NewScanner(body)
scanner.Buffer(make([]byte, 0, client.MaxEventBytes), client.MaxEventBytes)
上限一律命名常量,取自生产者上界。
上限命名规则
同一条流只有一个名字:
| 流 | 命名常量 | 值 | 来源 |
|---|---|---|---|
| SSE 事件行 | client.MaxEventBytes | 32MB | SSE 行长上限 |
read 工具单次 | — | 10MB | 工具契约 |
cmd/wesgine-accept 的 4MB 坐在 read 工具的 10MB 之下——一次大读会在终态 Done 之前中断,然后被报告成引擎缺陷。
截断处置
截断的处置必须显式选择,不能静默跳过:
致命(必须完整)
上游 LLM delta——tool_call 参数是 JSON 片段流,丢一片就拼不回来而流看起来一直健康。
跳过该条继续
wesgine 事件——自包含,丢一条只赔一条。
lineio 内部约束
internal/infra/lineio 内部禁止使用 Scanner:
- 否则会在每个迁移过来的调用方脚下重新装回上限
- 闸门单独设一条规则,堵"把 lineio 加进白名单让报错消失"这条出口
反模式速查
| 反模式 | 正确做法 |
|---|---|
bufio.NewScanner 读外部/无界输入 | 内存用 bytes.Lines;流式用 lineio |
| 对着 socket 用默认上限的 Scanner | 必须显式 .Buffer(...),上限用命名常量 |
| 超限当 EOF 处理 | 截断处置显式选择:致命或跳过继续 |
| 上限写成字面量 | 取自生产者上界,一律命名常量 |
lineio 内部用 Scanner | 闸门禁止 |
闸门
scripts/audit-lock/40-bounded-line-reads.sh 扫描全仓 Scanner 用法。