- 测试
- 操作系统
- 网络安全
【免费下载链接】syzkaller
syzkaller is an unsupervised coverage-guided kernel fuzzer
本文围绕 syzkaller 仓库中 vendor 目录下的 errchkjson 文档展开,系统讲解这一 Go 静态分析器如何检查传入encoding/json编码函数的参数类型、如何把返回的错误划分为“可安全忽略 / 必须检查 / 根本不支持”三类,并给出命令行参数、报告逻辑与源码级实现原理。读完后,你能够理解 syzkaller 这类大量使用 JSON 编码的工程,为何引入 errchkjson 作为间接依赖,以及如何将其纳入自己的 lint 流程(golangci-lint 或独立运行)。
一、问题背景:json.Marshal 的 err 到底要不要处理?
syzkaller 的 dashboard、manager、verifier 等组件中有大量json.Marshal调用(例如 dashboard/app/reporting.go、syz-manager/http.go、syz-verifier/monitoring_api.go),这些代码每天都在处理“错误值写不写 if 判断”的问题。errchkjson 的 README 用了一个典型例子来说明它要解决的矛盾:
func JSONHelloWorld(w http.ResponseWriter, r *http.Request) { response := struct { Message string Code int }{ Message: "Hello World", Code: 200, } body, err := json.Marshal(response) if err != nil { panic(err) // 一个只有 string 和 int 字段的 struct 永远不会 marshal 失败 } w.Write(body) }这里的panic实际上是不可能发生的,因此更地道的写法是直接丢弃错误:
body, _ := json.Marshal(response) w.Write(body)但直接写_是危险的:一旦日后有人给 response 结构体加了一个float64或interface{}字段,json.Marshal就可能真的返回错误(例如 NaN 编码失败),而被静默吞掉。errchkjson 的价值就在于把这两端都管起来——README 原文表述为:它检查传入 json 编码函数的类型,报告不受支持的类型,并报告哪些地方可以省略对返回错误的检查;一旦响应类型里加入了“不安全”的类型,linter 会立刻报警。换句话说,它让“body, _ := json.Marshal(x)”在类型可证明安全的前提下合法,在类型变得不安全时自动升级为 lint 错误。
二、errchkjson 在 syzkaller 中的位置
在 syzkaller 仓库中,errchkjson 并不是被直接调用的工具,而是作为golangci-lint 的内置 linter被间接 vendor 进来的。可以从以下三处佐证:
- 依赖声明:go.mod 第 68 行
github.com/breml/errchkjson v0.3.6 // indirect,go.sum 中有对应的校验和,说明它是 golangci-lint 拉进来的间接依赖; - 集成点:vendor/github.com/golangci/golangci-lint/pkg/golinters/errchkjson.go 中
NewErrChkJSON通过errchkjson.NewAnalyzer()构建 linter,并把 golangci-lint 的配置映射到分析器的 flag; - linter 注册:vendor/github.com/golangci/golangci-lint/pkg/lint/lintersdb/builder_linter.go 将
NewErrChkJSON(&cfg.LintersSettings.ErrChkJSON)注册为自 golangci-lint 1.44.0 起可用、归属bugs预设的 linter。
需要说明的是,syzkaller 当前的 .golangci.yml 中并没有显式 enableerrchkjson,因此它在本仓库中处于“可用但未启用”状态;而 syzkaller 自己的 lint 流程(Makefile 的lint目标)会从 vendor 目录安装golangci-lint并额外构建插件 tools/syz-linter。如果你希望在启用 errchkjson 的 golangci-lint 下审查类似 syzkaller 代码库中的json.Marshal用法,只需在 linters 配置中 enableerrchkjson并可选配置linters-settings.errchkjson即可(见下文第六节)。
三、安装与基本用法
README 给出的安装方式有两种:从 releases 下载二进制,或直接从源码安装:
go get github.com/breml/errchkjson/cmd/errchkjson基本用法就是对目标目录做全量检查:
errchkjson ./...它遵循golang.org/x/tools/go/analysis的 Analyzer 接口,因此既可以独立命令行运行,也可以被golangci-lint这类工具加载。分析器在 vendor/github.com/breml/errchkjson/errchkjson.go 中通过NewAnalyzer()构建,名称为errchkjson。
四、支持的命令行参数
README 提到-omit-safe这一个 flag,但从源码看分析器实际注册了三个(见 errchkjson.go#L32-L35):
| Flag | 默认值 | 作用 |
|---|---|---|
-omit-safe | false | 为true时,跳过“安全类型却仍检查错误值”这类报告(即只报 unsafe/unsupported,不再提示你去掉多余的 err 检查) |
-report-no-exported | false | 为true时,把“编码一个没有任何导出字段的 struct”也报为问题(会编码成空对象{},通常是笔误) |
-V | — | 打印版本并退出,版本信息定义在 version.go |
理解-omit-safe的关键在于:errchkjson 同时做双向检查——既报告“不安全却忽略了错误”(漏检查),也报告“安全却检查了错误”(冗余检查,默认开启)。-omit-safe=true后只保留前者,适合已经统一用errcheck类工具管“漏检查”、只想聚焦 JSON 类型安全的项目。golangci-lint 侧的配置项CheckErrorFreeEncoding正是这个 flag 的反向映射(见第六节)。
五、类型分类体系:Safe / Unsafe / Forbidden
这是 errchkjson 文档的核心。它对encoding/json编码可能性的分类如下,并与源码中的判定逻辑一一对应:
5.1 Safe(安全:编码不可能失败)
- 基本类型:
bool;int、int8~int64、uint、uint8~uint64、uintptr;string;以及上述基本类型的指针; - 复合类型(struct、map、slice、array)在值类型安全时安全;struct 只看导出字段;map 的 key 必须是整型或 string。
5.2 Unsafe(不安全:编码可能返回错误)
float32、float64(NaN/Inf 会触发json: unsupported value错误);interface{}(运行时值不可静态判定);- 上述类型的指针;
- 任何包含不安全基本类型的复合类型;
- 实现了
json.Marshaler或encoding.TextMarshaler接口的类型(例如json.Number)——因为它们的行为由用户自定义方法决定,linter 无法证明安全。
5.3 Forbidden(禁止:编码必然失败或无意义)
complex64、complex128、chan、func、unsafe.Pointer;- 包含上述禁止类型的任何复合类型;
- 使用禁止类型作为 key 的 map(
bool、float32、float64、struct)。
源码对照:分类是如何判定的
判定集中在 errchkjson.go 的jsonSafe递归函数中,可以逐段对照上面的分类表:
- 入口先查接口实现(L166-L168):类型若实现
TextMarshaler或Marshaler(接口在 L315-L348 中手工用go/types构造),直接返回 unsafe——这解释了为什么json.Number虽然是string的别名却被特殊处理:*types.Basic分支里有一条特判t.String() == "encoding/json.Number"视为 unsafe(L173-L175,map key 场景在 jsonSafeMapKey#L258-L278 中同样特判); - 基本类型(L171-L189):
allowedBasicTypes = IsBoolean | IsInteger | IsString命中即 safe;IsComplex走unsupported(对应 Forbidden);UnsafePointer也是unsupported;其余(float32/float64)返回 unsafe; - 复合类型(L191-L246):array/slice/pointer 递归检查元素类型;struct 先过滤非导出字段和
json:"-"tag 的字段再逐个检查(这正是“对 struct 只有导出字段相关”的规则来源),并用seenTypes集合处理递归类型防死循环;map 则用jsonSafeMapKey单独校验 key:只有整型和 string 合法,bool/float/struct 等 key 报unsupported ... as map key,interface key 报 unsafe; chan/func(L248-L250):types.Chan、types.Signature直接unsupported;- 兜底分支(L252-L254):其他类型(如 interface)视为 unsafe。
注意这里错误类型是三态的:unsafe(普通 error)与unsupported(unsupported_error.go 中的包装类型)会走不同的报告路径——unsupported无论错误是否被检查都会报告,因为它意味着这次 marshal必然出问题;而 unsafe 类型只有在错误被赋给_时才报告(见下一节)。noexported则是 noexported_error.go 定义的另一类包装错误,对应-report-no-exported的检查。
六、报告逻辑与“错误落在哪里”
handleJSONMarshal 是产生诊断的核心。它先用pass.TypesInfo.TypeOf(ce.Args[0])拿到 marshal 实参的类型(指针会先解引用一层),然后依据marshalErrorTarget(L110-L116)区分错误的去向:blankIdentifier(_)、variableAssignment(变量)或functionArgument(嵌套在其他函数调用参数里)。组合后得到 README 承诺的完整行为:
| 类型判定 | 错误目标 | 报告 |
|---|---|---|
| unsafe/unsupported | _ | “Error return value of%sis not checked: ...” |
| unsafe | 变量 | 不报告(检查了就是对的) |
| safe | 变量 | “Error return value of%sis checked but passed argument is safe”(-omit-safe时关闭此提示) |
| safe | _且-omit-safe=true | “Error return value of%sis not checked” |
| unsupported(如 chan/complex/func 入参) | 任意 | “%sfor %v”(必然失败,必须报) |
扫描入口 run 遍历每个文件的 AST,匹配三类目标函数:encoding/json.Marshal、encoding/json.MarshalIndent,以及(*encoding/json.Encoder).Encode。对后者的处理是omitSafe强制为true——因为Encode的错误还可能来自io.Writer,而不是 JSON 编码本身,所以“检查了错误但参数安全”这种提示对它没有意义。此外还有两个细节值得注意:
- 遇到
return语句即停止遍历(L49-L51),因为“把错误返回给调用者”本身就是一种检查,责任转移到调用方; - 对
Marshal调用出现在其他函数参数内部的嵌套情况,会进入 inspectArgs 递归处理,此时错误目标记为functionArgument,即只报 unsafe/unsupported,不报“冗余检查”。
七、golangci-lint 集成方式
golangci-lint 中的适配层 pkg/golinters/errchkjson.go 展示了配置如何映射到分析器 flag:
- 未显式配置时默认
omit-safe: true(即默认只报漏检查,不报冗余检查); - 配置
check-error-free-encoding: true时映射为omit-safe: false,开启“安全却仍检查错误”的双向提示; report-no-exported映射到-report-no-exported。
也就是说 golangci-lint 侧的配置项语义是:check-error-free-encoding(是否检查“本可不检查错误”的场景)与report-no-exported(是否报告无导出字段的 struct)。分析器以LoadModeTypesInfo模式加载,意味着依赖完整的类型信息,不能用于未通过类型检查的包。
八、已知的边缘情况
README 单独列出一个被接受(accepted)的边缘情况:json.MarshalIndent在 Go 1.18 时,如果参数嵌套深度超过 10000(src/encoding/json/scanner.go中的maxNestingDepth),即使是类型上完全安全的参数也可能返回错误(栈深超限)。这个病态场景无法通过静态类型分析发现,因此 errchkjson 不将其视为 unsafe——理解这一点有助于解释为什么该 linter 对MarshalIndent与Marshal采用相同的判定规则。
九、历史注脚:开发过程中发现的标准库问题
README 还记录了一个有分量的事实:在开发 errchkjson 期间,作者在 Go 标准库encoding/json中发现并推动了若干问题的修复,包括stringstruct tag 与SetEscapeHTML(false)的冲突(issue #34154)、json.Number带,stringoption 的错误编码(issue #34268 及其修复 PR)等。这段历史说明该工具的类型规则并非拍脑袋制定,而是与标准库的真实(含曾经出过 bug 的)行为反复校准的结果,也侧面解释了为什么规则要细致到json.Number这种别名级别。
十、小结与延伸阅读
结合本文,可以给出一个在类似 syzkaller 这种 JSON 密集型工程中的使用路径:
- 独立运行时:
go get github.com/breml/errchkjson/cmd/errchkjson后执行errchkjson ./...,用-omit-safe控制是否提示冗余检查; - 集成到 golangci-lint 时:enable
errchkjson(bugs 预设),并按需配置linters-settings.errchkjson.check-error-free-encoding与report-no-exported; - 审查规则细节时:直接读 errchkjson.go 的
jsonSafe/handleJSONMarshal两个函数,即可完整还原上文第四、五、六节的全部判定与报告逻辑; - 想了解 syzkaller 自身的 lint 管线如何构建,可参考 Makefile 的
lint目标与 .golangci.yml 中启用的 linter 清单。
核心要点一句话概括:errchkjson 用静态类型分析把“json.Marshal 的 err 该不该看”从风格争论变成了可机检规则——safe 类型可安全地_,unsafe 类型必须检查,unsupported 类型则根本不该出现在 marshal 参数里。
- 测试
- 操作系统
- 网络安全
【免费下载链接】syzkaller
syzkaller is an unsupervised coverage-guided kernel fuzzer
相关推荐
Infer 静态分析:CHECKERS_EXPENSIVE_OVERRIDES_UNANNOTATED 错误详解与 @Expensive 子类型规则
Infer 静态分析:CHECKERS_EXPENSIVE_OVERRIDES_UNANNOTATED 错误详解与 @Expensive 子类型规则 导读 本篇
静态分析代码质量开发工具PHPStan 错误详解:constructor.returnType —— 构造函数声明返回类型时的静态分析处理
PHPStan 错误详解:constructor.returnType —— 构造函数声明返回类型时的静态分析处理 constructor.returnType
开发工具代码质量静态分析PHPStan 错误详解:class.extendsFinalByPhpDoc——如何处理"继承 @final 类"的静态分析报告
PHPStan 错误详解:class.extendsFinalByPhpDoc——如何处理"继承 @final 类"的静态分析报告 导读 class.exten
开发工具代码质量静态分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考