☰
syzkaller 依赖治理工具 Depguard 深度解析:导入白名单机制、listMode 匹配算法与 golangci-lint 集成
2026/10/14 15:06:41 网站建设 项目流程
  • 测试
  • 操作系统
  • 网络安全

【免费下载链接】syzkaller

syzkaller is an unsupervised coverage-guided kernel fuzzer

项目地址:https://gitcode.com/gh_mirrors/syz/syzkaller
点击查看免费下载

本文以 syzkaller 仓库 vendor 目录中随 golangci-lint v1.57.2 一同引入的 depguard v2.2.0 依赖守卫工具为主线,完整讲解其配置文件语义(files/allow/deny/listMode、$gostd变量展开、glob 文件匹配)、三种列表模式的底层匹配算法与二进制搜索实现,并说明它在 syzkaller 的make lint流程中如何被 golangci-lint 调用,帮助你在大型 Go 项目中落地一套可审计的包导入约束。

Depguard 的定位一句话概括:一个校验包导入是否落在可接受列表内的 Go linter,你可以用它只放行某个组织下的全部仓库,也可以精确到仓库内的特定包。它同时支持两种使用形态——独立二进制(读取工作目录下的配置文件)和 golangci-lint 插件(通过analysis.Analyzer接口接入)。

一、定位与在 syzkaller 中的引入方式

Depguard 是一个基于golang.org/x/tools/go/analysis框架实现的analysis.Analyzer。在 syzkaller 仓库中,它并不是直接依赖,而是随 golangci-lint 工具链被间接引入的:

  • go.mod 中声明github.com/golangci/golangci-lint v1.57.2(直接依赖)以及github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect(间接依赖);
  • vendor/modules.txt 记录了 vendor 副本,包含depguard与depguard/internal/utils两个包;
  • Makefile 的lint目标负责构建并运行整套检查:
lint: # This should install the command from our vendor dir. CGO_ENABLED=1 $(HOSTGO) install github.com/golangci/golangci-lint/cmd/golangci-lint CGO_ENABLED=1 $(HOSTGO) build -buildmode=plugin -o bin/syz-linter.so ./tools/syz-linter bin/golangci-lint run $(LINT-FLAGS) ./...

可以看到 syzkaller 的make lint先安装 golangci-lint,再把自己独有的tools/syz-linter编译成-buildmode=plugin插件,最后运行golangci-lint run ./...——depguard 正是 golangci-lint 内置的众多 linter 之一(官方 README 也明确说明它 "built with Golangci-lint in mind")。而 syzkaller 的 .golangci.yml 当前在linters.disable列表中显式禁用了depguard(与gomodguard一并禁用),属于"引入但默认关闭"的状态。

独立部署方式(来自 depguard 官方 README):

go install github.com/OpenPeeDeeP/depguard@latest

二、Analyzer 入口与检查执行流程

从源码结构看,整个工具只有两个核心文件加一个内部工具包:

文件职责
vendor/github.com/OpenPeeDeeP/depguard/v2/depguard.goAnalyzer 构造、对每个文件导入的遍历与诊断上报
vendor/github.com/OpenPeeDeeP/depguard/v2/settings.go配置结构体、glob 编译、三种 listMode 的匹配算法
vendor/github.com/OpenPeeDeeP/depguard/v2/internal/utils/variables.go$all/$test/$gostd变量展开与 GOROOT 探测

2.1 两种 Analyzer 构造方式

depguard.go提供了两套构造函数,对应不同的生命周期控制需求:

  • NewAnalyzer(settings *LinterSettings):立即调用settings.compile()完成配置编译,编译失败直接返回 error;
  • NewUncompiledAnalyzer(settings):永不报错地返回UncompiledAnalyzer,把编译推迟到显式调用Compile()时——官方注释建议"在运行前调用 Compile",好处是配置编译只做一次,避免每次分析重复劳动。

底层统一注册为名字是depguard的analysis.Analyzer,Doc字段为 "Go linter that checks if package imports are in a list of acceptable packages",且RunDespiteErrors: false(类型检查有错的文件不跑)。

2.2 检查主循环:文件 → 列表 → 导入

run函数的核心逻辑(depguard.go#L68-L91)可以拆成四步:

  1. 遍历当前包的所有 AST 文件pass.Files;
  2. 用filepath.ToSlash统一路径分隔符(注释特别指出这是为了 Windows 兼容),再通过s.whichLists(fileName)找出该文件命中了哪些配置列表——注意一个文件可以同时命中多个列表,每个命中列表都会独立做一遍导入检查;
  3. 对该文件的每条file.Imports,在命中列表中逐一调用l.importAllowed(rawBasicLit(imp.Path)),其中rawBasicLit负责把导入路径的 AST 字面量去掉引号;
  4. 发现不允许的导入时构造analysis.Diagnostic并pass.Report。

诊断消息的格式是固定的:

import '<pkg>' is not allowed from list '<列表名>'

如果该包命中的是 deny 条目且配置了建议文本,消息会追加为... is not allowed from list 'Main': <建议>,同时生成一条analysis.SuggestedFix(这也是它能在支持 fix 的 CI 界面里展示修复建议的原因)。

三、配置文件:发现规则与完整字段语义

3.1 配置文件发现

Depguard 二进制在当前工作目录查找匹配正则^\.?depguard\.(yaml|yml|json|toml)$的文件,例如.depguard.yml、depguard.toml、depguard.json。也就是说YAML、YAML 点文件、JSON、TOML 四种格式等价——这从源码侧也能印证:List结构体的每个字段都同时携带四种标签(settings.go#L13-L18):

type List struct { ListMode string `json:"listMode" yaml:"listMode" toml:"listMode" mapstructure:"listMode"` Files []string `json:"files" yaml:"files" toml:"files" mapstructure:"files"` Allow []string `json:"allow" yaml:"allow" toml:"allow" mapstructure:"allow"` Deny map[string]string `json:"deny" yaml:"deny" toml:"deny" mapstructure:"deny"` }

顶层结构是LinterSettings map[string]*List:map 的 key 就是一个"列表名",会原样出现在 linter 输出的list '<name>'中;mapstructure标签则说明走 golangci-lint 配置注入时的字段绑定路径。

3.2 各字段含义与编译期行为

  • files:文件 glob 列表,用来决定哪些文件应用本列表的规则。三个要点:
    • 使用 gobwas/glob 库编译,分隔符指定为/;
    • 以!开头的 glob 进入"不匹配"(negFiles)列表,官方建议始终给 glob 加**/前缀,因为匹配是针对绝对路径做的;
    • files为空时,该列表匹配所有文件。
  • allow:允许导入的包前缀列表。编译时会做变量展开(如$gostd)并按字典序排序(为后面二进制搜索做准备)。
  • deny:不允许导入的包前缀 map,key 是包前缀、value 是给用户看的替代建议文本。编译时同样展开变量、排序,并维护一个与 deny 顺序对齐的suggestions切片。
  • listMode:包匹配优先级模式,取值不区分大小写,缺省为Original(向后兼容,官方明确"不推荐继续停留在这个模式");其余为Strict(默认拒绝:只有 allow 命中才放行)与Lax(默认放行:只有 deny 命中才拦截)。

编译期的一个硬约束值得注意:compile()末尾检查len(li.allow) == 0 && len(li.deny) == 0时直接报错must have an Allow and/or Deny package list(settings.go#L122-L125)——每个列表至少要有 allow 或 deny 其一,否则配置无效。

3.3 空配置的兜底:只放行标准库

LinterSettings.compile()里有一个容易被忽略的默认值逻辑(settings.go#L165-L178):当整个配置 map 为空时,自动生成一个名为Main的列表,files: ["$all"]、allow: ["$gostd"]。换句话说,什么也不写,等效于"所有文件只允许导入 Go 标准库"。

四、变量系统:$all、$test与$gostd的展开实现

官方 README 用变量来"减少重复和繁琐行为",实现全部集中在 internal/utils/variables.go。

4.1 文件变量

var ( PathExpandable = ExpanderMap{ "$all": &allExpander{}, "$test": &testExpander{}, } ... )
  • $all展开为**/*.go(匹配所有 Go 文件);
  • $test展开为**/*_test.go(匹配所有测试文件)。

变量可以直接被取反:!$test表示"排除所有测试文件"。展开发生在 glob 编译之前(settings.go中先utils.ExpandSlice再glob.Compile),所以变量最终只是"语法糖 + 预置 glob"。

4.2 包变量$gostd与 GOROOT 探测

$gostd不是简单的一条 glob,而是动态生成的一组包前缀:gostdExpander.Expand()读取GOROOT/src目录,把所有子目录名(os、net、encoding……)作为允许前缀返回,并带缓存。findGOROOT()的探测顺序是:环境变量GOROOT→runtime.GOROOT()(gccgo 特判)→ 执行go env GOROOT。注释里解释了这样做的依据:非标准库的导入必然以域名开头,或以./、/开头的本地路径引用,因此"标准库目录名集合"足以覆盖$gostd的语义。

4.3 三个官方配置示例

示例 1:主/测试分治 + 拒绝 reflect + 测试放行 testify

非测试文件命中Main,测试文件命中Test;两边都允许标准库但都拒绝reflect(并提示 "Please don't use reflect package");测试文件额外允许github.com/stretchr/testify及其任意子包。

Main: files: - $all - "!$test" allow: - $gostd deny: reflect: Please don't use reflect package Test: files: - $test allow: - $gostd - github.com/stretchr/testify deny: reflect: Please don't use reflect package

示例 2:多列表叠加(internal 目录双重约束)

所有文件命中Main,internal目录下的文件同时命中Main和Internal两个列表(两个列表的导入检查都会执行):

Main: files: - $all Internal: files: - "**/internal/**/*.go"

示例 3:精确拒绝单个包

$后缀表示"必须精确匹配":拒绝github.com/OpenPeeDeeP/depguard这个包本身,但github.com/OpenPeeDeeP/depguard/v2和github.com/OpenPeeDeeP/depguard/somepackage都会被放行(此处官方示例中 deny 写作列表形式,实际配置结构中 deny 是 map,语义等价于键为精确包路径的条目):

Main: deny: - github.com/OpenPeeDeeP/depguard$

官方还给出了一份 JSON 风格的全量示例,覆盖listMode: Strict(main 列表)与listMode: Lax(tests 列表)的组合:

{ "main": { "files": ["$all", "!$test"], "listMode": "Strict", "allow": ["$gostd", "github.com/OpenPeeDeeP"], "deny": { "reflect": "Who needs reflection" } }, "tests": { "files": ["$test"], "listMode": "Lax", "deny": { "github.com/stretchr/testify": "Please use standard library for tests" } } }

五、匹配算法源码解析:prefix list 与三种 listMode

Depguard 最有技术含量的部分在settings.go的两个函数:strInPrefixList(匹配原语)和importAllowed(模式判定)。

5.1 前缀匹配的二进制搜索实现

allow/deny是"包前缀列表":github.com/OpenPeeDeeP/depguard能匹配值github.com/OpenPeeDeeP,但不匹配github.com/OpenPeeDeeP/depguard/v2。匹配逻辑(settings.go#L224-L240):

func strInPrefixList(str string, prefixList []string) (bool, int) { // Idx represents where in the prefix slice the passed in string would go // when sorted. -1 Just means that it would be at the very front of the slice. idx := sort.Search(len(prefixList), func(i int) bool { return strings.TrimRight(prefixList[i], "$") > str }) - 1 if idx == -1 { return false, idx } ioc := prefixList[idx] if ioc[len(ioc)-1] == '$' { return str == ioc[:len(ioc)-1], idx } return strings.HasPrefix(str, prefixList[idx]), idx }

要点:

  1. 因为allow/deny在编译期已排序,这里用sort.Search做二分,找到"排序意义上应插入的位置的前一个元素"——只需检查这一个候选即可判定前缀关系,把逐条扫描优化成 O(log n);
  2. 尾部$的条目走精确匹配分支(去掉$后比较相等),否则走strings.HasPrefix;
  3. 返回值里带下标idx,专门给 listMode 的"更长前缀优先"裁决用。

5.2 三种 listMode 的裁决公式

当同一个包同时出现在 allow 与 deny 中(例如允许了某个子包却拒绝了它的父包,或反之),importAllowed(settings.go#L139-L159)按模式裁决:

switch l.listMode { case listModeOriginal: inAllowed = len(l.allow) == 0 || inAllowed allowed = inAllowed && !inDenied case listModeStrict: allowed = inAllowed && (!inDenied || len(l.allow[aIdx]) > len(l.deny[dIdx])) case listModeLax: allowed = !inDenied || (inAllowed && len(l.allow[aIdx]) > len(l.deny[dIdx])) }
  • Original:allow 列表为空视为"全部允许"(历史行为),命中 deny 即拒绝;
  • Strict:必须命中 allow,且当 deny 也命中时,allow 侧匹配到的前缀必须比 deny 侧更长才放行——即更具体的规则胜出;
  • Lax:默认放行,被 deny 命中时同样要求 allow 侧存在"更长"的匹配才能翻盘。

"更长前缀胜出"的设计与官方文档中"场景参考"的说法一致:depguard 仓库的settings_tests.go里TestListImportAllowed枚举了这类 allow/deny 交叠场景(该测试文件不在 syzkaller 的 vendor 副本中,vendor 只包含运行时源码)。文件命中判定fileMatch则很直白:inAllowed && !inDenied(settings.go#L133-L137),files为空时 inAllowed 恒真。

六、在 syzkaller 中的落地形态:golangci-lint 集成

Depguard 官方 README 的最后一节明确它是为 golangci-lint 设计的。在 syzkaller 仓库中,这体现在.golangci.yml的三个层面:

  1. 全局运行参数:run.deadline: 8m、skip-dirs(跳过pkg/kd与tools/syz-trace2syz)、build-tags: [codeanalysis](自动生成的 sys 描述文件体积巨大,靠构建标签排除,注释解释了原因)——depguard 若被启用,也会受这些全局约束;
  2. linter 开关:linters.enable里有 20 余个 linter(lll、vet、gofmt、revive、staticcheck、syz-linter……),而linters.disable中显式包含depguard与gomodguard。也就是说 syzkaller 团队评估过这两类"导入依赖管控"工具,当前选择保持禁用——从源码结构看,这可能与 syzkaller 的sys/目录由代码生成脚本批量产出、且大量跨平台文件需要按 target 组织导入有关;若要启用,需先在仓库根新增depguard.(yaml|yml|json|toml)配置并逐列表梳理各模块(dashboard/、pkg/、syz-manager/等)允许的外部依赖集合;
  3. 自定义插件的对照参照:linters-settings.custom.syz-linter.path: bin/syz-linter.so展示了 golangci-lint 的 custom linter 机制——depguard 走的是"内置 linter + 配置文件"路径,而tools/syz-linter走的是"插件二进制"路径,两者都汇入同一次golangci-lint run。

此外,syzzkaller 的 lint 输出在 CI 上会追加--out-format github-actions(Makefile 中ifdef CI分支的LINT-FLAGS),depguard 报告出的import 'X' is not allowed from list 'Main'诊断同样会进入该格式化通道。

七、实践要点小结

  • 配置最小集合:每个列表至少一个 allow 或 deny;files为空即全局生效;glob 记得加**/前缀、用!排除。
  • $后缀是精确匹配的唯一手段,allow/deny 两侧通用;不写$就是前缀匹配。
  • listMode 选择:存量项目从Lax(默认放行、deny 逐步收敛)起步,新项目直接用Strict(默认拒绝、allow 白名单);Original仅向后兼容,官方不推荐。
  • 一个文件可命中多个列表:例如"全量规则 + internal 加强规则"叠加使用时,两个列表的导入检查都会跑,deny 是取"任何列表拒绝即拒绝"的效果。
  • $gostd依赖 GOROOT 可探测:展开逻辑读取GOROOT/src目录名,因此标准库新增顶级目录后行为会自动跟上(有进程内缓存)。
  • 诊断信息自带列表名:from list '<name>'让多列表配置下的告警可定位到具体是哪组规则触发的。
  • 与 syzkaller 的关系:depguard 在 go.mod/vendor/modules.txt 中以 v2.2.0 间接存在,服务于 Makefilelint目标安装的那套 golangci-lint v1.57.2;当前 .golangci.yml 将其列入 disable,属于"随时可启用"的备选检查项。
  • 测试
  • 操作系统
  • 网络安全

【免费下载链接】syzkaller

syzkaller is an unsupervised coverage-guided kernel fuzzer

项目地址:https://gitcode.com/gh_mirrors/syz/syzkaller
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询