Syzkaller 安装

先安装软件

sudo apt-get install debootstrap
sudo apt install qemu-kvm
sudo apt-get install subversion
sudo apt-get install git
sudo apt-get install make
sudo apt-get install qemu
sudo apt install libssl-dev libelf-dev
sudo apt-get install flex bison libc6-dev libc6-dev-i386 linux-libc-dev linux-libc-dev:i386 libgmp3-dev libmpfr-dev libmpc-dev
apt-get install g++
apt-get install build-essential
apt install gcc

安装go

add-apt-repository ppa:longsleep/golang-backports
apt-get update
sudo apt-get install golang-go
//go的版本为1.19

然后设置goproxy

go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct

然后go get源代码

go get -u -d github.com/google/syzkaller/prog

进入后进行编译

image-20230202163605050

发现报错,

dmesg | egrep -i -B100 'killed process'
执行命令 发现 OOM-Killer

image-20230202164905655

重新分配,16G,编译成功

当然也可以 建立swap分区

https://studygolang.com/articles/11781?fr=sidebar

image-20230202165719303

编译完成

文件系统

我们新建一个 image文件夹,下载create-image.sh 但是

https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh

网络问题,我们手动下载

# 安装debootstrap
sudo apt install debootstrap
# 下载脚本
wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
# 添加可执行权限
chmod +x create-image.sh
# 使用清华源,不然慢死了
sed -i -e 's~sudo debootstrap .*~\0 https://mirrors.tuna.tsinghua.edu.cn/debian/~' create-image.sh
# 制作镜像,1024MB
./create-image.sh -s 1024

执行会有报错

image-20230202184647993

由于windows系统下换行符为 \r\n,linux下换行符为 \n,所以导致在windows下编写的文件会比linux下多回车符号 \r。
只需要去掉多余的 \r 回车符 即可。操作办法可以用sed命令进行全局替换
sed 's/\r//' -i gen_cert.sh

image-20230202185212833

内核

https://mirrors.edge.kernel.org/pub/linux/kernel

手动下载 或者wget

image-20230202185333825

# 先采用默认配置
make defconfig
# 启用kvm
make kvmconfig
# Syzkaller需要启用一些调试功能
echo '
CONFIG_KCOV=y
CONFIG_DEBUG_INFO=y
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y
CONFIG_CONFIGFS_FS=y
CONFIG_SECURITYFS=y' >> .config
# 再次对新引入的配置采用默认值
make olddefconfig

使用qemu进入测试,成功

root 密码空

image-20230202204536871

qemu-system-x86_64 -m 1G \
	-enable-kvm \
	-drive file=/home/test/go/src/github.com/google/syzkaller/image/stretch.img,format=raw \
	-kernel ./linux-4.4.146/arch/x86/boot/bzImage \
	-append root=/dev/sda

我们在syzkaller 中生成我们的cfg文件

{
    "target": "linux/amd64",
    "http": "0.0.0.0:8080",
    "workdir": "/home/test/go/src/github.com/google/syzkaller/bin/workdir",
    "kernel_obj": "/home/test/桌面/cheche/kernel/linux-4.4.146/",
    "image": "../image/stretch.img",
    "sshkey": "../image/stretch.id_rsa",
    "syzkaller": "/home/test/go/src/github.com/google/syzkaller",
    "enable_syscalls": ["chmod"],
    "procs": 1,
    "type": "qemu",
    "vm": {
        "count": 1,
        "kernel": "/home/test/桌面/cheche/kernel/linux-4.4.146/arch/x86/boot/bzImage",
        "cpu": 1,
        "mem": 1024
    }
}

image-20230202205559264

./syz-manager -config 4.14.cfg -vv 10

image-20230202220955391

config 字段的解释

https://github.com/google/syzkaller/blob/master/pkg/mgrconfig/config.go

Android common kernel

https://android.googlesource.com/kernel/common/

挂代理 git 所有后 才能进行查看

git log --all | grep 搜索 

image-20230206185802747

image-20230206185813253

git log查看的不全

我们搜索到之后

精确下载 某个版本

image-20230206190404803

proxychains git clone -b ASB-2018-08-05_4.4 https://android.googlesource.com/kernel/common

安装多次之后终于下载成功

image-20230206211105877

image-20230206211115821

参考文献

赛兹卡勒/setup_ubuntu-host_qemu-vm_x86-64-kernel.md at 大师 ·谷歌/Syzkaller ·GitHub

syzkaller/setup.md at master · google/syzkaller · GitHub

https://bbs.kanxue.com/thread-265405.htm#%E5%B0%9D%E8%AF%95%E4%BB%8E0%E5%88%B01%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8syzkaller%E8%BF%9B%E8%A1%8Clinux%E5%86%85%E6%A0%B8%E6%BC%8F%E6%B4%9E%E6%8C%96%E6%8E%98

https://snappyjack.github.io/articles/2020-05/%E4%BD%BF%E7%94%A8Syzkaller%E8%BF%9B%E8%A1%8C%E5%86%85%E6%A0%B8fuzz

https://i-m.dev/posts/20200313-143737.html

https://blingblingxuanxuan.github.io/2019/10/26/syzkaller/

ARM syzkaller

http://wanjiabing.top/posts/zh/kerneldebug/syzkaller/

安卓模拟

https://www.owalle.com/2020/05/11/android-emulator/

http://pwn4.fun/2019/10/29/Syzkaller-Fuzz-Android-Kernel/

https://source.android.com/docs/core/tests/debug/kasan-kcov?hl=zh-cnzsy

syzkaller 源码阅读笔记-1

前言

syzkaller 是 google 开源的一款无监督覆盖率引导的 kernel fuzzer,支持包括 Linux、Windows 等操作系统的测试。

syzkaller 有很多个部件。其中:

  • syz-extract:用于解析 syzlang 中的常量
  • syz-sysgen:用于解析 syzlang,提取其中描述的 syscall 和参数类型,以及参数依赖关系
  • syz-manager:用于启动与管理 syzkaller
  • syz-fuzzer:实际在 VM 中运行的 fuzzer
  • syz-executor:实际在 VM 中运行的测试程序

image-20230209124913289

commit 14a312c837f1ebfece99a5cac64d37eba33654af

功能总结:编译系统调用模板的原理,可以理解成syzkaller实现了一种描述系统调用的小型的编程语言。

  • syz-extract :根据 syzlang 文件从内核源文件中提取出使用的对应的宏、系统调用号等的值,生成 .const 文件(例如,xxx.txt.const)。
  • syz-sysgen:通过 syzlang 文件与 .const 文件进行,语法分析与语义分析,生成抽象语法树,最终生成供 syzkaller 使用的 golang 代码,分为如下四个步骤:
    • assignSyscallNumbers:分配系统调用号,检测不支持的系统调用并丢弃;
    • patchConsts:将 AST 中的常量替换为对应的值;
    • check:进行语义分析;
    • genSyscalls:从 AST 生成 prog 对象。

syz-extract

用途:解析并获取 syzlang 文件中的常量所对应的具体整型,并将结果存放至 xxx.txt.const 文件中

syz-extract main 函数位于 sys/syz-extract/extract.go 中。

开头导入了一些包,我们 暂且不看
import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"

	"github.com/google/syzkaller/pkg/ast"
	"github.com/google/syzkaller/pkg/compiler"
	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/pkg/tool"
	"github.com/google/syzkaller/sys/targets"
)

main

在main函数中

func main() {
	flag.Parse()
	if *flagBuild && *flagBuildDir != "" {
		tool.Failf("-build and -builddir is an invalid combination")
	}

syz-extract 会尝试解析传入的参数,也就是flag

直接定义了我们的参数 和他的提示信息以及默认值

var (
	flagOS        = flag.String("os", runtime.GOOS, "target OS")
	flagBuild     = flag.Bool("build", false, "regenerate arch-specific kernel headers")
	flagSourceDir = flag.String("sourcedir", "", "path to kernel source checkout dir")
	flagIncludes  = flag.String("includedirs", "", "path to other kernel source include dirs separated by commas")
	flagBuildDir  = flag.String("builddir", "", "path to kernel build dir")
	flagArch      = flag.String("arch", "", "comma-separated list of arches to generate (all by default)")
)
  • flagOS:是一个字符串类型的变量,默认值是当前系统的操作系统(runtime.GOOS)。它定义了命令行参数 “os”,表示目标操作系统。

  • flagBuild:是一个布尔类型的变量,默认值是 false。它定义了命令行参数 “build”,表示是否重新生成特定架构的内核头文件。

  • flagSourceDir:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “sourcedir”,表示内核源代码的存储路径。

  • flagIncludes:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “includedirs”,表示其他内核源代码包含目录,多个目录用逗号隔开。

  • flagBuildDir:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “builddir”,表示内核生成的文件存储路径。

  • flagArch:是一个字符串类型的变量,默认值是空字符串。它定义了命令行参数 “arch”,表示需要生成的架构,多个架构用逗号隔开,如果不指定则生成所有架构。

    这些值来自于go中的flag包

image-20230209173925099

​ 接下来,便是尝试获取 OS 所对应的 Extractor 结构体;如果 OS 不存在则肯定取不到,直接报错:

	OS := *flagOS
	extractor := extractors[OS]
	if extractor == nil {
		tool.Failf("unknown os: %v", OS)
	}

​ extractors 数组如下所示,该数组为不同的 OS 实例化了不同的 Extractor 类。其中 linux OS 所对应的 Extractor 实例(即那三个函数的实现)位于 sys/syz-extract/linux.go 中:

type Extractor interface {
	prepare(sourcedir string, build bool, arches []*Arch) error
	prepareArch(arch *Arch) error
	processFile(arch *Arch, info *compiler.ConstInfo) (map[string]uint64, map[string]bool, error)
}

var extractors = map[string]Extractor{
	targets.Akaros:  new(akaros),
	targets.Linux:   new(linux),
	targets.FreeBSD: new(freebsd),
	targets.Darwin:  new(darwin),
	targets.NetBSD:  new(netbsd),
	targets.OpenBSD: new(openbsd),
	"android":       new(linux),
	targets.Fuchsia: new(fuchsia),
	targets.Windows: new(windows),
	targets.Trusty:  new(trusty),
}

​ 回到 main 函数,syz-extract 要用已有的 OS 字符串、archArray 字符串数组,以及 syzlang 文件名数组来生成出对应的 arches 结构体数组

​ 在旧的版本中,会有单另的archFileList用来生成arch字符串,file等

​ 现在 合并在createArches中,archList函数之类的。

用已有的 OS 字符串、archArray 字符串数组(调用archList()获得),以及 syzlang 文件名数组来生成出对应的 arches 结构体数组

arches, nfiles, err := createArches(OS, archList(OS, *flagArch), flag.Args())
	if err != nil {
		tool.Fail(err)
	}
	if *flagSourceDir == "" {
		tool.Fail(fmt.Errorf("provide path to kernel checkout via -sourcedir " +
			"flag (or make extract SOURCEDIR)"))
	}
  • OS 为操作系统字符串
  • archList结果为待生成的 arch 字符串数组
  • nfiles 为待分析的 syzlang 文件名 字符串数组

准备工作已经做的差不多了,接下来让 extractor 执行初始化操作:

	if err := extractor.prepare(*flagSourceDir, *flagBuild, arches); err != nil {
		tool.Fail(err)
	}

这一步实际上会调用到 sys/syz-extract/linux.go 中的 prepare 函数:

func (*linux) prepare(sourcedir string, build bool, arches []*Arch) error {
	if build {
		// Run 'make mrproper', otherwise out-of-tree build fails.
		// However, it takes unreasonable amount of time,
		// so first check few files and if they are missing hope for best.
		for _, a := range arches {
			arch := a.target.KernelArch
			if osutil.IsExist(filepath.Join(sourcedir, ".config")) ||
				osutil.IsExist(filepath.Join(sourcedir, "init/main.o")) ||
				osutil.IsExist(filepath.Join(sourcedir, "include/config")) ||
				osutil.IsExist(filepath.Join(sourcedir, "include/generated/compile.h")) ||
				osutil.IsExist(filepath.Join(sourcedir, "arch", arch, "include", "generated")) {
				fmt.Printf("make mrproper ARCH=%v\n", arch)
				out, err := osutil.RunCmd(time.Hour, sourcedir, "make", "mrproper", "ARCH="+arch,
					"-j", fmt.Sprint(runtime.NumCPU()))
				if err != nil {
					return fmt.Errorf("make mrproper failed: %v\n%s", err, out)
				}
			}
		}
	} else {
		if len(arches) > 1 {
			return fmt.Errorf("more than 1 arch is invalid without -build")
		}
	}
	return nil
}

如果不指定重新生成 linux kernel header,那么只会做一些简单的检查。但如果指定重新生成了,则会尝试在 linux kernel src 上执行 make mrproper

回到 main 函数,接下来便是创建 go routine 通信管道和启动并行 worker:

go routine 是 go 的轻量级线程,其中关键字 go 后面的语句将被放进新的 go routine 中执行。

	jobC := make(chan interface{}, len(arches)+nfiles)
	for _, arch := range arches {
		jobC <- arch
	}

	for p := 0; p < runtime.GOMAXPROCS(0); p++ {
		go worker(extractor, jobC)
	}

上面的代码创建了一个管道 jobC,该管道的容量为所有架构数量加上文件数量。然后,它循环所有的架构,并将每个架构作为接口类型的值放入该管道中。

接着,它循环该代码在一个系统上可以同时运行的最大的处理数,并在每次循环中启动一个新的工作程序,并将 extractorjobC 作为参数传递给该程序。这个工作程序的目的是从 jobC 管道中提取架构,并运行提取程序。

因此,通过创建多个工作程序,代码可以并行地处理所有架构。

总的来说,如果有多个架构,则启动多线程并发执行各自的 processArch() / processFile()

​ worker 启动后,main 函数就需要等待 worker 处理完成后才能保存处理结果至文件中,这就涉及到了线程协同。注意到代码中有 <-arch.done<-f.done 语句,这两个语句会一直阻塞等待管道,直到其传来信息。若 worker 函数中对管道执行 close 操作,则被关闭的管道将不再等待,继续向下执行。因此这里 syz-extract 就利用了管道来完成线程协同。

constFiles := make(map[string]*compiler.ConstFile)
	for _, arch := range arches {
		fmt.Printf("generating %v/%v...\n", OS, arch.target.Arch)
		<-arch.done
		if arch.err != nil {
			failed = true
			fmt.Printf("%v\n", arch.err)
			continue
		}
		for _, f := range arch.files {
			<-f.done
			if f.err != nil {
				failed = true
				fmt.Printf("%v: %v\n", f.name, f.err)
				continue
			}
			if constFiles[f.name] == nil {
				constFiles[f.name] = compiler.NewConstFile()
			}
			constFiles[f.name].AddArch(f.arch.target.Arch, f.consts, f.undeclared)
		}
	}

剩下的部分就是将生成结果保存在const文件中

for file, cf := range constFiles {
		outname := filepath.Join("sys", OS, file+".const")
		data := cf.Serialize()
		if len(data) == 0 {
			os.Remove(outname)
			continue
		}
		if err := osutil.WriteFile(outname, data); err != nil {
			tool.Failf("failed to write output file: %v", err)
		}
	}

	if !failed && *flagArch == "" {
		failed = checkUnsupportedCalls(arches)
	}
	for _, arch := range arches {
		if arch.build {
			os.RemoveAll(arch.buildDir)
		}
	}
	if failed {
		os.Exit(1)
	}

main函数的主要逻辑如下:

  1. 首先,调用flag.Parse()来解析命令行参数,主要是OS,arch,syzlang文件名。

  2. 检查传入的参数是否合法:如果flagBuildflagBuildDir同时出现,输出错误信息;如果没有提供操作系统的类型,也输出错误信息。

  3. 获取参数OS的值,并通过extractors字典来获取对应的提取器。如果没有对应的提取器,输出错误信息。

  4. 通过createArches函数生成需要处理的架构,并向jobC channel 中添加需要处理的任务。见 createArches()

  5. sys/syz-extract/linux.go: prepare() —— 初始化操作,如果设置了 build 参数,表示重新生成特定架构的内核头文件,先删除之前编译所生成的文件和配置文件;

  6. 启动GOMAXPROCS(0)个工作协程,它们从jobC channel 中读取任务并处理。

  7. 对每种arch架构,多线程并发执行 worker()(边进行常量提取,边将先前已有的提取结果存放进文件中,提高效率),真正执行变量解析工作;—— 见 1-4 processArch()

    • sys/syz-extract/extract.go: processArch():处理传入的 Extractor 和 Arch 结构体,生成 const 信息。
      • pkg/ast/parser.go: ParseGlob() :将编写的txt文件解析成AST;
      • pkg/compiler/consts.go: ExtractConsts():从每个syzlang文件中提取出const值;返回 syzlang 文件名与其用到的常量数组的映射;
      • sys/syz-extract/linux.go: prepareArch():补全某些 arch 的 kernel src 可能会缺失的头文件;
    • sys/syz-extract/linux.go: processFile():编译生成可执行文件,并搜集常量;
      • sys/syz-extract/fetch.go: extract():主要函数。

    8.等待 worker() 多线程执行完成,结果保存到 const 文件。

总体流程

  • 调用自定义 compiler 解析 syzlang 为 AST 森林,并依次提取每个 AST 树上的 consts 节点,然后将这些 consts 节点上的字符串放置进模板中,编译模板生成一个 ELF 或其他可执行文件;
  • 分析 ELF 文件上的数据,或者尝试执行可执行文件来解析其输出,以获得各个 consts 字符串所对应的具体整型值;
  • 将获取到的 consts 字符串与具体整型的映射关系,一个个序列化并填入 .const 文件中,这样便生成了对应于每个 syzlang 文件的 .const 文件。

archList

获取架构 name list

功能:确定待分析的目标架构,如果指定了架构则直接返回,如果未指定架构则返回所有架构的架构name数组。注意所有架构的信息保存在 sys/targets/targets.go: targets.List 中。

参数:OS 字符串、arch 字符串。

代码如下

func archList(OS, arches string) []string {
	if arches != "" {
		return strings.Split(arches, ",")
	}
	var archArray []string
	for arch := range targets.List[OS] {
		archArray = append(archArray, arch)
	}
	sort.Strings(archArray)
	return archArray
}

targets.List 如下

var List = map[string]map[string]*Target{
	...
	Linux: {
		AMD64: {
			PtrSize:          8,
			PageSize:         4 << 10,
			LittleEndian:     true,
			CFlags:           []string{"-m64"},
			Triple:           "x86_64-linux-gnu",
			KernelArch:       "x86_64",
			KernelHeaderArch: "x86",
			NeedSyscallDefine: func(nr uint64) bool {
				// Only generate defines for new syscalls
				// (added after commit 8a1ab3155c2ac on 2012-10-04).
				return nr >= 313
			},
		},
		I386: {
			VMArch:           AMD64,
			PtrSize:          4,
			PageSize:         4 << 10,
			Int64Alignment:   4,
			LittleEndian:     true,
			CFlags:           []string{"-m32"},
			Triple:           "x86_64-linux-gnu",
			KernelArch:       "i386",
			KernelHeaderArch: "x86",
		},
		ARM64: {
			PtrSize:          8,
			PageSize:         4 << 10,
			LittleEndian:     true,
			Triple:           "aarch64-linux-gnu",
			KernelArch:       "arm64",
			KernelHeaderArch: "arm64",
		},
		ARM: {
			VMArch:           ARM64,
			PtrSize:          4,
			PageSize:         4 << 10,
			LittleEndian:     true,
			CFlags:           []string{"-D__LINUX_ARM_ARCH__=6", "-march=armv6"},
			Triple:           "arm-linux-gnueabi",
			KernelArch:       "arm",
			KernelHeaderArch: "arm",
		},
		...
}

createArches

功能:生成与参数对应的 Arch 结构体数组。

注:syzlang 可以用来写syscall模板

syzlang 是 syzkaller 中的一个组件,它提供了一种高级语言,用于描述系统调用和系统数据结构。这种语言称为 syzlang,并且它抽象了底层细节,方便描述复杂的系统调用和数据结构。它使用起来更方便,并且可以在不涉及技术细节的情况下描述系统调用。

func createArches(OS string, archArray, files []string) ([]*Arch, int, error) {
	errBuf := new(bytes.Buffer)
	//报错函数
	eh := func(pos ast.Pos, msg string) {
		fmt.Fprintf(errBuf, "%v: %v\n", pos, msg)
	}
	top := ast.ParseGlob(filepath.Join("sys", OS, "*.txt"), eh)
	if top == nil {
		return nil, 0, fmt.Errorf("%v", errBuf.String())
	}
	allFiles := compiler.FileList(top, OS, eh)
	if allFiles == nil {
		return nil, 0, fmt.Errorf("%v", errBuf.String())
	}
	nfiles := 0
	var arches []*Arch
	for _, archStr := range archArray { // [1] 遍历架构 name 数组
		buildDir := "" // [2] 确定 build 文件夹路径
		if *flagBuild {
			dir, err := ioutil.TempDir("", "syzkaller-kernel-build")
			if err != nil {
				return nil, 0, fmt.Errorf("failed to create temp dir: %v", err)
			}
			buildDir = dir
		} else if *flagBuildDir != "" {
			buildDir = *flagBuildDir
		} else {
			buildDir = *flagSourceDir
		}

		target := targets.Get(OS, archStr) // [3] 获取 targets.List 中对应与 OS 和 arch 的 `Target` 结构体
		if target == nil {
			return nil, 0, fmt.Errorf("unknown arch: %v", archStr)
		}

		arch := &Arch{ // [4] 创建 arch 结构体
			target:      target,          // 存放特定 OS 特定 arch 的一些信息
			sourceDir:   *flagSourceDir,  // kernel source 路径
			includeDirs: *flagIncludes,   // kernel source header 路径
			buildDir:    buildDir,        // build 路径
			build:       *flagBuild,      // bool 值,是否需要重新生成架构指定的 kernel header
			done:        make(chan bool), // 管道,用于 go routine 间通信。当 arch 分析完成后,将会向该管道通知
		}
		archFiles := files
		if len(archFiles) == 0 {
			for file, meta := range allFiles {
				if meta.NoExtract || !meta.SupportsArch(archStr) {
					continue
				}
				archFiles = append(archFiles, file)
			}
		}
		sort.Strings(archFiles)
		for _, f := range archFiles { // [5] 将 syzlang 文件名数组添加进 arch 结构体中
			arch.files = append(arch.files, &File{ //将文件的信息(通过 File 对象)附加到 "arch.files" 列表
				arch: arch,
				name: f,
				done: make(chan bool),// 管道,用于 go routine 间通信。当 file 分析完成后,将会向该管道通知
			})
		}
		arches = append(arches, arch)
		nfiles += len(arch.files)
	}
	return arches, nfiles, nil
}

它叫做 “createArches”,接受三个参数:

  1. “OS” - 字符串类型,代表操作系统的名称。
  2. “archArray” - 字符串数组,代表你想要构建的架构。
  3. “files” - 字符串数组,代表要打包的文件。

它返回两个结果:

  1. []*Arch - 一个指针数组,代表创建的架构。
  2. int - 一个整数,代表打包后的文件数量。
  3. error - 错误信息,如果出现错误,则返回错误信息。

例如,你可以调用该函数如下:

os := "Linux"
archArray := []string{"x86", "x64"}
files := []string{"file1.txt", "file2.txt"}
arches, count, err := createArches(os, archArray, files)
if err != nil {
    fmt.Println(err)
} else {
    fmt.Println("Arches:", arches)
    fmt.Println("Count:", count)
}

worker

功能:执行真正的变量解析工作。分别对Arch和 syzlang File 调用 processArch() 函数和 processFile() 函数处理。

参数: 传给 worker()jobC 参数就是 Arch 结构体数组。所以在 worker() 函数中会进入 case *Arch 分支。

func worker(extractor Extractor, jobC chan interface{}) {
	for job := range jobC {
		switch j := job.(type) { // [1] j 赋值为 jobC 管道中的对象,初始时为 Arch 结构体
		case *Arch:
			infos, err := processArch(extractor, j) // [2] 执行 processArch(), 生成 const 信息
			j.err = err
			close(j.done)
			if j.err == nil {
				for _, f := range j.files {
					f.info = infos[filepath.Join("sys", j.target.OS, f.name)]
					jobC <- f // [3] processArch() 执行完后,从 infos 映射中遍历取出对应文件的信息,并将其填充至 arch 结构体中 files 结构体数组内的各个元素字段里; 将这个 File 结构体放入 jobC 管道中
				} //"jobC <- f" 表示将一个 "f" 变量写入 "jobC" 通道。
			}
		case *File:
			j.consts, j.undeclared, j.err = processFile(extractor, j.arch, j)
			close(j.done)
		}
	}
}

​ 该函数在一个 for 循环中不断读取 “jobC” 通道中的任务,并对其进行处理。每个任务是一个接口类型,该程序通过一个 switch 语句判断每个任务的具体类型。

​ 如果任务的类型为 *Arch,则使用 “processArch” 函数处理该任务,并关闭 “j.done” 通道,如果 “j.err” 等于 nil,则对每个 “j.files” 中的文件再次进行处理并写入 “jobC” 通道。

​ 如果任务的类型为 *File,则使用 “processFile” 函数处理该任务,并将处理结果写入 “j.consts”,“j.undeclared” 和 “j.err” 字段,然后关闭 “j.done” 通道。

这个代码是一个并行处理任务的示例,通过不断读取通道中的任务并处理,实现了并行的效果。

流程说明:由于 worker() 会循环读取 jobC 内数据,因此接下来便会取出刚刚新放入的 File 结构体,执行 processFile() 函数。在 processFile() 中,syz-extract 将会获取各个 const 变量(例如 O_RDWR)所对应的整型值(例如2)。

注意worker() 中需注意,当 processFile() 执行完成后,worker 函数接下来都会执行 close(j.done) ,将通信管道关闭。这样做的是为了通知 main() 函数 goroutine “某部分工作已经完成”。这个操作有点类似于使用信号量来保证线程同步。

processArch

功能:processArch 的作用是,处理传入的 Extractor 和 Arch 结构体,生成 const 信息。

func processArch(extractor Extractor, arch *Arch) (map[string]*compiler.ConstInfo, error) {
	errBuf := new(bytes.Buffer)
	// 定义 error handler 函数
	eh := func(pos ast.Pos, msg string) {
		fmt.Fprintf(errBuf, "%v: %v\n", pos, msg)
	}
	// 解析 sys/linux/*.txt 的 syzlang 文件,形成一个 AST 数组
	// 因此 top 变量就是 ast 森林的根节点
	top := ast.ParseGlob(filepath.Join("sys", arch.target.OS, "*.txt"), eh)
	if top == nil {
		return nil, fmt.Errorf("%v", errBuf.String())
	}
	// 调用 compiler.ExtractConsts 获取每个 syzlang 文件中所对应的 const 信息
	infos := compiler.ExtractConsts(top, arch.target, eh)
	if infos == nil {
		return nil, fmt.Errorf("%v", errBuf.String())
	}
	// 让 Extractor 为 arch 做些准备
	if err := extractor.prepareArch(arch); err != nil {
		return nil, err
	}
	return infos, nil //将获取到的consts infos 返回给调用者
}
  • 调用 pkg/ast/parser.go: ParseGlob() -> pkg/ast/parser.go: Parse() 将编写的txt文件解析成AST。

    • Parse() -> parseTopRecover() 解析出节点加入到top中,并且会在struct前后加上空行,移除重复的空行。
    • parseTopRecover() -> parseTop() 根据标识符的类型调用不同的函数处理。
  • 调用了库函数 pkg\compiler\const.gocompiler.ExtractConsts() ,主要调用pkg\compiler\compiler.go Compile() 提取出常量标识符。返回编译 syzlang 结果中的 res.fileConsts 字段.

    • ExtractConsts() -> Compile()
      • createCompiler() :在 syscall_descriptions_syntax.md 中可以看到syzkaller内建的一些别名和模板,在 createCompiler() 函数中对它们进行了初始化。
      • typecheck():分别调用 checkDirectives()checkNames()checkFields()checkTypedefs()checkTypes() 这五个函数进行一些检查。对于可能出现的错误可以对照consts_errors.txt,errors.txt和errors2.txt中给出的例子。
      • extractConsts():返回提取const值所需的文本常量和其它信息的列表(负责提取目录/头文件/定义的name/系统调用名/call/struct/resource中的常量)。列表中的内容分别为常量(consts),定义(defines),包含头文件数组(includeArray),包含目录数组(incdirArray)。

    其中,compiler.ExtractConsts 只是一个简单的 wrapper 函数,获取编译 syzlang 结果中的 fileConsts 字段:

    func ExtractConsts(desc *ast.Description, target *targets.Target, eh ast.ErrorHandler) map[string]*ConstInfo {
    	res := Compile(desc, nil, target, eh)
    	if res == nil {
    		return nil
    	}
    	return res.fileConsts
    }
    

image-20230211211258346

​ 字段 res.fileConsts 包含了 syzlang 文件名与其用到的常量数组的映射,以及其所 include 的头文件数组的映射;这些东西都将会用到获取 consts 对应的具体整数操作中。

extractor.prepareArch 函数在 linux.go 中,做的操作主要是定义了几个头文件:

"stdarg.h": `
#pragma once
#define va_list __builtin_va_list
#define va_start __builtin_va_start
#define va_end __builtin_va_end
#define va_arg __builtin_va_arg
#define va_copy __builtin_va_copy
#define __va_copy __builtin_va_copy
`,

"asm/a.out.h":    "",
"asm/prctl.h":    "",
"asm/mce.h":      "",
"uapi/asm/msr.h": "",

因为某些 arch 的 kernel src 可能会缺失这些文件,需要自己手动补全。补全之后 extractor.prepareArch 会重新执行一次 linux kernel make 生成。

回到 processArch 函数,该函数最后会把先前获取到的 consts info 返回给调用者:

processFile

编译并搜集常量

功能sys/syz-extract/extract.go: processFile() 只是封装了 sys/syz-extract/linux.go: processFile()。查找const值(主要在 [3] 处调用 sys/syz-extract/fetch.go: extract() 函数)。

说明:最后生成的 res 映射和 undeclared 集合。res 是 const 字符串与整型的映射;undeclared 是未声明 const 字符串与 bool 值的映射,通常这里的 bool 值都为 true。

undeclared 所对应的常量将在 .const 文件中标明其值为 ???,例如

O_RDWR = 2 MyConst = ???

type Extractor interface {
	prepare(sourcedir string, build bool, arches []*Arch) error
	prepareArch(arch *Arch) error
	processFile(arch *Arch, info *compiler.ConstInfo) (map[string]uint64, map[string]bool, error)
}

func processFile(extractor Extractor, arch *Arch, file *File) (map[string]uint64, map[string]bool, error) {
	inname := filepath.Join("sys", arch.target.OS, file.name)
	if file.info == nil {
		return nil, nil, fmt.Errorf("const info for input file %v is missing", inname)
	}
	if len(file.info.Consts) == 0 {
		return nil, nil, nil
	}
	return extractor.processFile(arch, file.info)
}
//sys/syz-extract/linux.go: processFile()
func (*linux) processFile(arch *Arch, info *compiler.ConstInfo) (map[string]uint64, map[string]bool, error) {
	headerArch := arch.target.KernelHeaderArch // [1] 生成编译代码模板所用到的 gcc 编译参数:args
	sourceDir := arch.sourceDir
	buildDir := arch.buildDir
	args := []string{
		// This makes the build completely hermetic, only kernel headers are used.
		"-nostdinc",
		"-w", "-fmessage-length=0",
		"-O3", // required to get expected values for some __builtin_constant_p
		"-I.",
		"-D__KERNEL__",
		"-DKBUILD_MODNAME=\"-\"",
		"-I" + sourceDir + "/arch/" + headerArch + "/include",
		"-I" + buildDir + "/arch/" + headerArch + "/include/generated/uapi",
		"-I" + buildDir + "/arch/" + headerArch + "/include/generated",
		"-I" + sourceDir + "/arch/" + headerArch + "/include/asm/mach-malta",
		"-I" + sourceDir + "/arch/" + headerArch + "/include/asm/mach-generic",
		"-I" + buildDir + "/include",
		"-I" + sourceDir + "/include",
		"-I" + sourceDir + "/arch/" + headerArch + "/include/uapi",
		"-I" + buildDir + "/arch/" + headerArch + "/include/generated/uapi",
		"-I" + sourceDir + "/include/uapi",
		"-I" + buildDir + "/include/generated/uapi",
		"-I" + sourceDir,
		"-I" + sourceDir + "/include/linux",
		"-I" + buildDir + "/syzkaller",
		"-include", sourceDir + "/include/linux/kconfig.h",
	}
	args = append(args, arch.target.CFlags...)
	for _, incdir := range info.Incdirs {
		args = append(args, "-I"+sourceDir+"/"+incdir)
	}
	if arch.includeDirs != "" {
		for _, dir := range strings.Split(arch.includeDirs, ",") {
			args = append(args, "-I"+dir)
		}
	}
	params := &extractParams{ // [2] 准备 extract 参数: params, 准备待使用的CC编译器
		AddSource:      "#include <asm/unistd.h>",
		ExtractFromELF: true,
		TargetEndian:   arch.target.HostEndian,
	}
	cc := arch.target.CCompiler
	res, undeclared, err := extract(info, cc, args, params) // [3] 执行核心函数 extract,生成 res 映射和 undeclared 集合
	if err != nil {
		return nil, nil, err
	}
	if arch.target.PtrSize == 4 { // [4] 若当前架构是32位, 则 syz-extract 需要使用 mmap2 来替换 mmap,以避免一些可能的错误
		// mmap syscall on i386/arm is translated to old_mmap and has different signature.
		// As a workaround fix it up to mmap2, which has signature that we expect.
		// pkg/csource has the same hack.
		const mmap = "__NR_mmap"
		const mmap2 = "__NR_mmap2"
		if res[mmap] != 0 || undeclared[mmap] {
			if res[mmap2] == 0 {
				return nil, nil, fmt.Errorf("%v is missing", mmap2)
			}
			res[mmap] = res[mmap2]
			delete(undeclared, mmap)
		}
	}
	return res, undeclared, nil // [5] 返回结果
}

核心代码extract 是这个

	params := &extractParams{ // [2] 准备 extract 参数: params, 准备待使用的CC编译器
		AddSource:      "#include <asm/unistd.h>",
		ExtractFromELF: true,
		TargetEndian:   arch.target.HostEndian,
	}
	cc := arch.target.CCompiler
	res, undeclared, err := extract(info, cc, args, params) // [3] 执行核心函数 extract,生成 res 映射和 undeclared 集合
	if err != nil {
		return nil, nil, err
	}

image-20230211212645060

image-20230211212701105

extract

编译并搜集常量

位置sys/syz-extract/fetch.go

功能:调用编译器来编译代码模板,并根据编译出的二进制文件来获取 consts 常量整数。若编译过程出错,则会尝试自动纠错。

参数:Info 便是单个文件存放 const 数据的结构体,cc 是编译器名称字符串,args 是编译器执行参数,params 是用于 extract 执行过程用的选项。

func extract(info *compiler.ConstInfo, cc string, args []string, params *extractParams) (
	map[string]uint64, map[string]bool, error) {
	data := &CompileData{ // [1] 初始化: 声明一系列的 map
		extractParams: params,
		Defines:       info.Defines,
		Includes:      info.Includes,
		Values:        info.Consts,
	}
	// 编译生成的程序路径
	bin := ""
	// 这个字段貌似没有用途,先行忽略
	missingIncludes := make(map[string]bool)
	// 未定义的 const,通常是自己定义的常量
	undeclared := make(map[string]bool)
	// 声明并初始化 valMap 中各个元素为 true
	valMap := make(map[string]bool)
	for _, val := range info.Consts {
		valMap[val] = true
	}
	for {
		// [2] 尝试将 consts 常量字符串与模板C代码结合,并编译结合后的代码,形成一个可执行文件
		bin1, out, err := compile(cc, args, data) // [2-1] 编译操作, 返回结果分别为编译出的可执行文件路径 / 编译器标准输出信息 / 编译器标准错误信息
		if err == nil {
			bin = bin1
			break
		}
		// Some consts and syscall numbers are not defined on some archs.
		// Figure out from compiler output undefined consts,
		// and try to compile again without them.
		// May need to try multiple times because some severe errors terminate compilation.
		tryAgain := false
		for _, errMsg := range []string{ // [2-2] 遍历所有预先定义的错误信息,并使用正则表达式匹配
			`error: [‘']([a-zA-Z0-9_]+)[’'] undeclared`,
			`note: in expansion of macro [‘']([a-zA-Z0-9_]+)[’']`,
			`note: expanded from macro [‘']([a-zA-Z0-9_]+)[’']`,
			`error: use of undeclared identifier [‘']([a-zA-Z0-9_]+)[’']`,
		} {
			re := regexp.MustCompile(errMsg)
			matches := re.FindAllSubmatch(out, -1)
			for _, match := range matches { // [2-3] 如果匹配到了,则将出问题的常量存于 undeclared 中
				val := string(match[1])
				if valMap[val] && !undeclared[val] {
					undeclared[val] = true
					tryAgain = true
				}
			}
		}
		if !tryAgain {
			return nil, nil, fmt.Errorf("failed to run compiler: %v %v\n%v\n%s",
				cc, args, err, out)
		}
		data.Values = nil               // 重置编译用的 consts 数组
		for _, v := range info.Consts { // [2-4] 将出错的 consts 剔除,并将剩余没出错的 consts 存入编译用的 consts 数组
			if undeclared[v] {
				continue
			}
			data.Values = append(data.Values, v)
		}
		data.Includes = nil
		for _, v := range info.Includes {
			if missingIncludes[v] {
				continue
			}
			data.Includes = append(data.Includes, v)
		}
	}
	defer os.Remove(bin) // [3] 将新编译出的二进制文件删除

	var flagVals []uint64
	var err error
	if data.ExtractFromELF { // [4] 从编译出的二进制文件中读取数值,解析并返回
		flagVals, err = extractFromELF(bin, params.TargetEndian) // [4-1] OS 为 Linux 时, 走这个分支,不会实际执行程序,而是从 ELF 文件中一个名为 syz_extract_data 的 section 中读取常量值
	} else {
		flagVals, err = extractFromExecutable(bin) // 若 ExtractFromELF 字段为 false, 实际执行目标程序,解析其输出并转换为整型数组
	}
	if err != nil {
		return nil, nil, err
	}
	if len(flagVals) != len(data.Values) {
		return nil, nil, fmt.Errorf("fetched wrong number of values %v, want != %v",
			len(flagVals), len(data.Values))
	}
	res := make(map[string]uint64)
	for i, name := range data.Values {
		res[name] = flagVals[i]
	}
	return res, undeclared, nil
}

因为上面提到了compile函数,我们进行查看

sys/syz-extract/fetch.go: compile()

功能:将 consts 常量字符串与模板C代码结合,并编译结合后的代码,形成一个可执行文件。

说明:模板C代码存于 srcTemplate 变量,该模板会将先前从 syzlang 收集到的 include、define 和 consts 字符串全部融合:

  • 如果设置了 ExtractFromELF 标志位,则 consts 值将全部放置在一个名为 syz_extract_data 的 section 上
  • 如果没有设置该标志位,则编译出来的程序在执行时将会依次打印 consts 值,以 %llu 的输出格式&使用空格来区分每个变量,输出至 stdout中。这样,sys-extract 就可以通过分析所编译程序的输出,来确定每个 consts 字符串所对应的数值是多少。
func compile(cc string, args []string, data *CompileData) (string, []byte, error) {
    // 创建填充好后的 C 代码缓冲区
    src := new(bytes.Buffer)
    // 使用传入的 data 对代码模板 srcTemplate 进行填充
    if err := srcTemplate.Execute(src, data); err != nil {
        return "", nil, fmt.Errorf("failed to generate source: %v", err)
    }
    // 创建一个临时可执行文件路径
    binFile, err := osutil.TempFile("syz-extract-bin")
    if err != nil {
        return "", nil, err
    }
    // 为编译器添加额外的参数
    args = append(args, []string{
        // -x c :指定代码语言为 C 语言
        // - :指定代码从标准输入而不是从文件中读取
        "-x", "c", "-",
        // 指定文件输出的路径
        "-o", binFile,
        "-w",
    }...)
    if data.ExtractFromELF {
        // gcc -c 参数:只编译但不链接
        // 由于我们测试时使用的是 Linux,因此会进入该分支
        args = append(args, "-c")
    }
    // 执行程序
    cmd := osutil.Command(cc, args...)
    // 将填充后的代码模板喂给 gcc 编译
    cmd.Stdin = src
    // 将 stdin 和 stdout 的输入糅合,使得他俩的输出完全一致
    // 通俗的说就是让 stdin 和 stdout 都指向同一个管道
    if out, err := cmd.CombinedOutput(); err != nil {
        os.Remove(binFile)
        return "", out, err
    }
    return binFile, nil, nil
}

执行至 compile的图

image-20230211214831671

代码模板 如下

var srcTemplate = template.Must(template.New("").Parse(`
{{if not .ExtractFromELF}}
#define __asm__(...)
{{end}}

{{if .DefineGlibcUse}}
#ifndef __GLIBC_USE
#    define __GLIBC_USE(X) 0
#endif
{{end}}

{{range $incl := $.Includes}}
#include <{{$incl}}>
{{end}}

{{range $name, $val := $.Defines}}
#ifndef {{$name}}
#    define {{$name}} {{$val}}
#endif
{{end}}

{{.AddSource}}

{{if .DeclarePrintf}}
int printf(const char *format, ...);
{{end}}

{{if .ExtractFromELF}}
__attribute__((section("syz_extract_data")))
unsigned long long vals[] = {
    {{range $val := $.Values}}(unsigned long long){{$val}},
    {{end}}
};
{{else}}
int main() {
    int i;
    unsigned long long vals[] = {
        {{range $val := $.Values}}(unsigned long long){{$val}},
        {{end}}
    };
    for (i = 0; i < sizeof(vals)/sizeof(vals[0]); i++) {
        if (i != 0)
            printf(" ");
        printf("%llu", vals[i]);
    }
    return 0;
}
{{end}}
`))

可以很容易的看出来,该模板会将先前从 syzlang 收集到的 include、define 和 consts 字符串全部融合:

  • 如果设置了 ExtractFromELF 标志位,则 consts 值将全部放置在一个名为 syz_extract_data 的 section 上
  • 如果没有设置该标志位,则编译出来的程序在执行时将会依次打印 consts 值,以 %llu 的输出格式&使用空格来区分每个变量,输出至 stdout中。这样,sys-extract 就可以通过分析所编译程序的输出,来确定每个 consts 字符串所对应的数值是多少。

checkUnsupportedCalls

func checkUnsupportedCalls(arches []*Arch) bool {
	supported := make(map[string]bool)
	unsupported := make(map[string]string)
	for _, arch := range arches {
		for _, f := range arch.files {
			for name := range f.consts {
				supported[name] = true
			}
			for name := range f.undeclared {
				unsupported[name] = f.name
			}
		}
	}
	failed := false
	for name, file := range unsupported {
		if supported[name] {
			continue
		}
		failed = true
		fmt.Printf("%v: %v is unsupported on all arches (typo?)\n",
			file, name)
	}
	return failed
}
  1. 首先,使用 make 函数创建两个 map,一个是 supported,一个是 unsupported。supported 用来存储已经支持的名称,unsupported 用来存储未支持的名称。
  2. 然后,对于 arches 中的每个架构,遍历该架构的所有文件,并对这些文件中的常量和未声明的变量进行处理。如果是常量,则将其名称添加到 supported 中;如果是未声明的变量,则将其名称和对应的文件名添加到 unsupported 中。
  3. 最后,对于 unsupported 中的每个未支持的变量,如果该变量的名称在 supported 中,则说明该变量是支持的;否则,打印出该变量不支持的错误信息。

最终,返回该函数是否有失败(failed)。如果 failed 为 true,则说明存在不支持的调用;否则,说明所有的调用都是支持的。

archList

func archList(OS, arches string) []string {
	if arches != "" {
		return strings.Split(arches, ",")
	}
	var archArray []string
	for arch := range targets.List[OS] {
		archArray = append(archArray, arch)
	}
	sort.Strings(archArray)
	return archArray
}

archList 用来返回archArray 简单的拆分

小结

syz-extract 会调用自定义 compiler 解析 syzlang 为 ast 森林,并依次提取每个 ast 树上的 consts 节点,然后将这些 consts 节点上的字符串放置进模板中,编译模板生成一个 ELF 或其他可执行文件。

接下来 syz-extract 会分析 ELF 文件上的数据,或者尝试执行可执行文件来解析其输出,以获得各个 consts 字符串所对应的具体整型值。

最后 syz-extract 将获取到的 consts 字符串与具体整型的映射关系,一个个序列化并填入 .const 文件中,这样便生成了对应于每个 syzlang 文件的 .const 文件。

在 syz-extract 执行的整个过程中,syz-extract 另起一个 go routine 来执行 worker,是为了能达到边进行常量提取,边将先前已有的提取结果存放进文件中,这样做是为了提高效率,加快常量提取的速度。

syz-sysgen

位置sys/syz-sysgen/sysgen.go

功能解析人工编写的syzlang代码文件,并将syzlang内部定义的syscall类型信息转换成后续syzkaller能够使用的数据结构。简单地说,syz-sysgen 解析 syzlang 文件,并为 syz-fuzzer 和 syz-executor 的编译运行做准备。

main

func main() {
	defer tool.Init()()

	var OSList []string
	for OS := range targets.List {
		OSList = append(OSList, OS)
	}
	sort.Strings(OSList)

	data := &ExecutorData{}

首先,使用 defer 关键字和 tool.Init() 函数在 main 函数结束之前初始化某些工具。

defer 关键字在 Go 语言中用于延迟函数的执行。当遇到 defer 语句时,Go 程序会将该语句所在的函数的执行推迟到函数返回时再执行。

举个例子,如果你有一个文件需要打开,并在程序执行完毕后关闭,你可以使用 defer 来做到这一点:

f, err := os.Open("file.txt")
if err != nil {
 log.Fatal(err)
}
defer f.Close()
// 程序将在这里执行其他操作,而不是在这里关闭文件

这样做的优势在于,即使程序需要从多个不同的地方返回,您仍然可以确保文件将在最终关闭。

func Init() func() {
	flagCPUProfile := flag.String("cpuprofile", "", "write CPU profile to this file")
	flagMEMProfile := flag.String("memprofile", "", "write memory profile to this file")
	if err := ParseFlags(flag.CommandLine, os.Args[1:]); err != nil {
		Fail(err)
	}
	return installProfiling(*flagCPUProfile, *flagMEMProfile)
}

涉及到cpuprofile 和memprofile 暂时不看

根据ki 爷的图 执行到这里

image-20230212154821430

紧接着便是一个 for 循环,遍历 OSList 中的每个 OS 字符串,并解析其中的 syzlang 代码。将这个 for 循环分为了上中下三个部分:

第一部分

	for _, OS := range OSList { // [2] for 循环,遍历OSList中每个OS字符串,并解析其中的syzlang代码
		descriptions := ast.ParseGlob(filepath.Join(*srcDir, "sys", OS, "*.txt"), nil)
		if descriptions == nil { // [2-1] syzlang文件解析成AST数树
			os.Exit(1)
		}
		constFile := compiler.DeserializeConstFile(filepath.Join(*srcDir, "sys", OS, "*.const"), nil)
		if constFile == nil { // .const 文件解析成 ConstFile 结构体
			os.Exit(1)
		}
		osutil.MkdirAll(filepath.Join(*outDir, "sys", OS, "gen")) // syz-sysgen 输出结果存放在本目录

		var archs []string
		for arch := range targets.List[OS] {
			archs = append(archs, arch)
		}
		sort.Strings(archs)
...

​ 这部分内容较为简单,将当前遍历到的 OS 所对应的 sys/<os>/*.txtsys/<os>/*.const文件,分别解析成 AST 树 (ast.Description 类型) 和 ConstFile 结构体。之后创建 sys/<os>/gen 文件夹,整个 syz-sysgen 的输出将存放在该文件夹下:

偷KI爷的图

image-20230212160403886

之后还是收集当前 OS 所对应的全部 arch 字符串集合,并做一次排序操作。

第二部分

for _, OS := range OSList {
...
var jobs []*Job // [2-2] 为每个arch都创建1个Job结构体, 将其添加进数组jobs中, 并为数组执行排序操作
		for _, arch := range archs {
			jobs = append(jobs, &Job{
				Target:      targets.List[OS][arch],
				Unsupported: make(map[string]bool),
			})
		}
		sort.Slice(jobs, func(i, j int) bool {
			return jobs[i].Target.Arch < jobs[j].Target.Arch
		})
		var wg sync.WaitGroup // sync.WaitGroup 结构体, 用于等待指定数量的 go routine 集合执行完成, 类似于信号量
		wg.Add(len(jobs))     // wg.Add(): 增加内部计数器值; wg.Done(): 减小内部计数器值; wg.Wait():判断内部计数器值状态, 进而选择是否挂起等待

		for _, job := range jobs { // 遍历 jobs 数组中每个 job, 创建 go routine 并行执行这些 job
			job := job
			go func() {
				defer wg.Done()
				processJob(job, descriptions, constFile) // processJob() 重要函数
			}()
		}
		wg.Wait()
...
}

​ 首先是为每个 arch 都创建了一个 Job 结构体,将其添加进数组 jobs中,并为数组执行排序操作,其中排序规则是自定义的。

​ 接下来创建了一个 sync.WaitGroup 结构体,这个结构体用于等待指定数量的 go routine 集合执行完成。其内部原理有点类似于信号量,执行 wg.Add 函数以增加其内部计数器值,执行 wg.Done 函数以减小其内部计数器值,执行 wg.Wait 则判断内部计数器值状态,进而选择是否挂起等待。

​ 其中最重要的是,syz-sysgen 依次遍历 jobs 数组中的每个 job,并创建 go routine 并行执行这些 job。函数 processJob 用于编译先前 parse 的 syzlang AST、分析其中的类型信息与依赖关系,并将其序列化为 golang 代码至 sys/<OS>/gen/<arch>.go 中,同时还将 syscall 属性相关的信息保存在 job.ArchData 中,供后续生成 sys-executor 关键头文件代码所用。

第三部分

for _, OS := range OSList {
    ...
    
    var syscallArchs []ArchData
    unsupported := make(map[string]int)
    for _, job := range jobs {
        if !job.OK {
            fmt.Printf("compilation of %v/%v target failed:\n", job.Target.OS, job.Target.Arch)
            for _, msg := range job.Errors {
                fmt.Print(msg)
            }
            os.Exit(1)
        }
        syscallArchs = append(syscallArchs, job.ArchData)
        for u := range job.Unsupported {
            unsupported[u]++
        }
    }
    data.OSes = append(data.OSes, OSData{
        GOOS:  OS,
        Archs: syscallArchs,
    })

    for what, count := range unsupported {
        if count == len(jobs) {
            tool.Failf("%v is unsupported on all arches (typo?)", what)
        }
    }
}

​ 第三部分没什么需要特别关注的,这部分主要是做了一些检查,并将先前 worker 里生成的 ArchData 提取进变量 data 中。

for 循环结束后吗,main 函数最后这部分的代码继续为变量 data 设置一些字段:

	attrs := reflect.TypeOf(prog.SyscallAttrs{}) // [3] 分别将 prog.SyscallAttrs 和 prog.CallProps 这两个结构体对应的字段名存起来
	for i := 0; i < attrs.NumField(); i++ {
		data.CallAttrs = append(data.CallAttrs, prog.CppName(attrs.Field(i).Name))
	}

	props := prog.CallProps{}
	props.ForeachProp(func(name, _ string, value reflect.Value) {
		data.CallProps = append(data.CallProps, CallPropDescription{
			Type: value.Kind().String(),
			Name: prog.CppName(name),
		})
	})

	writeExecutorSyscalls(data)
}

​ 这部分代码只是分别将 prog.SyscallAttrsprog.CallProps 这两个结构体对应的字段名存了起来。俩结构体声明如下:

// SyscallAttrs represents call attributes in syzlang.
//
// This structure is the source of truth for the all other parts of the system.
// pkg/compiler uses this structure to parse descriptions.
// syz-sysgen uses this structure to generate code for executor.
//
// Only `bool`s and `uint64`s are currently supported.
//
// See docs/syscall_descriptions_syntax.md for description of individual attributes.
type SyscallAttrs struct {
	Disabled      bool
	Timeout       uint64
	ProgTimeout   uint64
	IgnoreReturn  bool
	BreaksReturns bool
	NoGenerate    bool
	NoMinimize    bool
}

prog\prog.go
// These properties are parsed and serialized according to the tag and the type
// of the corresponding fields.
// IMPORTANT: keep the exact values of "key" tag for existing props unchanged,
// otherwise the backwards compatibility would be broken.
type CallProps struct {
	FailNth int  `key:"fail_nth"`
	Async   bool `key:"async"`
	Rerun   int  `key:"rerun"`
}

image-20230212165940203

通过对上面源码的分析,我发现 syz-sysgen 将整个 prog.SyscallAttrs 结构体的字段名和每个 syscall 所对应的数据,全都转换成了普通字符串型和整型。看上去这像是要用这些数据来填充 C 语言模板?我们接下来再来看看 writeExecutorSyscalls 函数,看看这里面具体是做了什么。

writeExecutorSyscalls 函数源码分析位于下文,这里不再赘述。

processJob()

功能:编译传入的 syzlang AST,分析其中的 syscall 类型信息等,并反序列化为一个 golang 语法源码。

参数:传入的参数 job ,结构体声明如下:

type Job struct {
    Target      *targets.Target // 存放着一些关于特定 OS 特定 arch 的一些常量信息
    OK          bool
    Errors      []string        // 保存报错信息的字符串集合,一条字符串表示一行报错信息
    Unsupported map[string]bool // 存放不支持的 syscall 集合
    ArchData    ArchData        // 存放待从 worker routine 返回给 main 函数的数据
}

首先,该函数会生成一个 error handler,用于输出错误信息;之后从 ConstFile 结构体中,取出对应 arch 的 consts 字符串->整型映射表:

eh := func(pos ast.Pos, msg string) { // [1] 生成一个 error handler, 用于输出错误信息;
		job.Errors = append(job.Errors, fmt.Sprintf("%v: %v\n", pos, msg))
	}

image-20230212174737319

func processJob(job *Job, descriptions *ast.Description, constFile *compiler.ConstFile) {
	eh := func(pos ast.Pos, msg string) { // [1] 生成一个 error handler, 用于输出错误信息;
		job.Errors = append(job.Errors, fmt.Sprintf("%v: %v\n", pos, msg))
	}
	consts := constFile.Arch(job.Target.Arch) // [2] 从 constFile 结构体取出对应 arch 的 consts 字符串->整型 映射表
	if job.Target.OS == targets.TestOS {      // [3] 过滤掉自己开发人员测试用的 testOS (targets.TestOS 即为字符串 test)
		constInfo := compiler.ExtractConsts(descriptions, job.Target, eh)
		compiler.FabricateSyscallConsts(job.Target, constInfo, consts)
	}
	prog := compiler.Compile(descriptions, consts, job.Target, eh) // [4] 对 syzlang AST 进行编译, 继续分析 AST 信息。
	if prog == nil {                                               // 这次编译提供了consts信息,因此会执行完整的编译过程
		return
	}
	for what := range prog.Unsupported {
		job.Unsupported[what] = true
	}
	// [5] 将分析结果,序列化为go语言源代码,留待后续 syz-fuzzer 使用,代码存放在 sys/<OS>/gen/<arch>.go
	sysFile := filepath.Join(*outDir, "sys", job.Target.OS, "gen", job.Target.Arch+".go")
	out := new(bytes.Buffer)
	generate(job.Target, prog, consts, out)
	rev := hash.String(out.Bytes())
	fmt.Fprintf(out, "const revision_%v = %q\n", job.Target.Arch, rev)
	writeSource(sysFile, out.Bytes())
	// [6] 调用 generateExecutorSyscalls 函数来创建 Executor 的 syscall 信息,并将其返回给 main 函数
	job.ArchData = generateExecutorSyscalls(job.Target, prog.Syscalls, rev)

	// Don't print warnings, they are printed in syz-check.
	job.Errors = nil
	job.OK = true
}

syz-sysgen 需要分析 AST 信息,对 syzlang 进行编译:

prog := compiler.Compile(descriptions, consts, job.Target, eh) // [4] 对 syzlang AST 进行编译, 继续分析 AST 信息。
	if prog == nil {                                               // 这次编译提供了consts信息,因此会执行完整的编译过程
		return
	}
	for what := range prog.Unsupported {
		job.Unsupported[what] = true
	}

返回的 Prog 结构体声明如下:

// Prog is description compilation result.
type Prog struct {
    Resources []*prog.ResourceDesc
    Syscalls  []*prog.Syscall
    Types     []prog.Type
    // Set of unsupported syscalls/flags.
    Unsupported map[string]bool
    // Returned if consts was nil.
    fileConsts map[string]*ConstInfo
}

​ [4]编译操作和先前 syz-extract 类似,不同的是这次提供了 consts 信息,因此会执行完整的编译过程,分析 syzlang 代码中描述的全部 syscall 参数类型信息。返回的 Prog 结构体中:

  • 字段 fileConsts 为空
  • 涉及到的类型信息保存在了 Resource 和 Types 字段
  • syscall 的描述则存放在 Syscalls 字段中。

Compile() 除了调用 createCompiler() 函数和 typecheck() 函数,接下来首先调用的是assignSyscallNumbers() / patchConsts() / check() 函数。

  • assignSyscallNumbers() 函数分配系统调用号,检测不受支持的系统调用并丢弃。
  • patchConsts() 函数将AST中的常量patch成对应的值。
  • check() 函数对AST进行语义检查。
  • genSyscalls() 主要是调用了 genSyscall() 函数,然后按照系统调用名排序。
  • genSyscall() 函数中调用 genType() 函数生成返回值,调用 genFieldArray() 函数生成每个参数。
  • 返回的 Prog 对象中调用 genResources() 函数生成资源,generateTypes() 函数生成结构体的描述。

我们来看看生成出的 golang 代码是什么样的(以 /sys/linux/gen/amd64.go 为例):

说明

  • 开头的 init() 函数用于将当前这个 linux amd64 的 target,注册进 targets 数组中以供后续 syz-fuzzer 取出使用。
  • 其中声明了多个数组:
    • resources_amd64 数组:存放着每个 syzlang 代码中声明的 resource 变量
    • syscalls_amd64 数组:存放着每个 syscall 所对应的名称、调用号,以及各个参数的名称和类型。
    • types_amd64 数组:每个类型的具体信息,例如数组、结构体类型信息等等
    • consts_amd64:存放 consts 字符串与整型的映射关系
    • revision_amd64:amd64.go 源码的哈希值
// AUTOGENERATED FILE
// +build !codeanalysis
// +build !syz_target syz_target,syz_os_linux,syz_arch_amd64

package gen

import . "github.com/google/syzkaller/prog"
import . "github.com/google/syzkaller/sys/linux"

func init() {
    RegisterTarget(&Target{OS: "linux", Arch: "amd64", Revision: revision_amd64, PtrSize: 8, PageSize: 4096, NumPages: 4096, DataOffset: 536870912, LittleEndian: true, ExecutorUsesShmem: true, Syscalls: syscalls_amd64, Resources: resources_amd64, Consts: consts_amd64}, types_amd64, InitTarget)
}

var resources_amd64 = []*ResourceDesc{
{Name:"ANYRES16",Kind:[]string{"ANYRES16"},Values:[]uint64{18446744073709551615,0}},
{Name:"ANYRES32",Kind:[]string{"ANYRES32"},Values:[]uint64{18446744073709551615,0}},
{Name:"ANYRES64",Kind:[]string{"ANYRES64"},Values:[]uint64{18446744073709551615,0}},
{Name:"IMG_DEV_VIRTADDR",Kind:[]string{"IMG_DEV_VIRTADDR"},Values:[]uint64{0}},
{Name:"IMG_HANDLE",Kind:[]string{"IMG_HANDLE"},Values:[]uint64{0}},
{Name:"assoc_id",Kind:[]string{"assoc_id"},Values:[]uint64{0}},
....
}

var syscalls_amd64 = []*Syscall{
{NR:43,Name:"accept",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11199)},
{Name:"peer",Type:Ref(10021)},
{Name:"peerlen",Type:Ref(10305)},
},Ret:Ref(11199)},
{NR:43,Name:"accept$alg",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11202)},
{Name:"peer",Type:Ref(4943)},
{Name:"peerlen",Type:Ref(4943)},
},Ret:Ref(11203)},
{NR:43,Name:"accept$ax25",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11204)},
{Name:"peer",Type:Ref(10033)},
{Name:"peerlen",Type:Ref(10305)},
},Ret:Ref(11204)},
{NR:43,Name:"accept$inet",CallName:"accept",Args:[]Field{
{Name:"fd",Type:Ref(11223)},
{Name:"peer",Type:Ref(10025)},
{Name:"peerlen",Type:Ref(10305)},
},Ret:Ref(11223)},
....
}

var types_amd64 = []Type{
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(17155)},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14707),Kind:1,RangeEnd:32},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14707),Kind:1,RangeEnd:8},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14560)},
&ArrayType{TypeCommon:TypeCommon{TypeName:"array",TypeAlign:1,IsVarlen:true},Elem:Ref(14575)},
....
}

var consts_amd64 = []ConstValue{
{"ABS_CNT",64},
{"ABS_MAX",63},
{"ACL_EXECUTE",1},
{"ACL_GROUP",8},
{"ACL_GROUP_OBJ",4},
{"ACL_LINK",1},
....
}

const revision_amd64 = "e61403f96ca19fc071d8e9c946b2259a2804c68e"

generateExecutorSyscalls()

功能:为生成 syz-executor 准备相关的 syscall 数据,因此起名神似 生成(generate) executor 的 syscall 数据。具体来说,就是遍历 Syscall,将对应的 SyscallData 添加到 data.Calls

func generateExecutorSyscalls(target *targets.Target, syscalls []*prog.Syscall, rev string) ArchData {
	data := ArchData{ // [1] 创建 ArchData结构体,该结构体最后会返回给 main()
		Revision:   rev,
		GOARCH:     target.Arch,
		PageSize:   target.PageSize,
		NumPages:   target.NumPages,
		DataOffset: target.DataOffset,
	}
	if target.ExecutorUsesForkServer { // 若目标 OS & arch 对应的target结构体,设置了对 ForkServer 和 Shmem(共享内存)的支持, 则设置data中相应字段, 这样 syz-executor便能使用这两种技术加速fuzz
		data.ForkServer = 1
	}
	if target.ExecutorUsesShmem {
		data.Shmem = 1
	}
	defines := make(map[string]string)
	for _, c := range syscalls { // [2] 遍历各个 Syscall 类型的结构体
		var attrVals []uint64
		attrs := reflect.ValueOf(c.Attrs) // 将变量 c 中结构体 SyscallAttrs 里的各个字段取出,并将其依次存放至整型数组 attrVals (bool值和整型值)
		last := -1
		for i := 0; i < attrs.NumField(); i++ {
			attr := attrs.Field(i)
			val := uint64(0)
			switch attr.Type().Kind() {
			case reflect.Bool:
				if attr.Bool() {
					val = 1
				}
			case reflect.Uint64:
				val = attr.Uint()
			default:
				panic("unsupported syscall attribute type")
			}
			attrVals = append(attrVals, val)
			if val != 0 {
				last = i
			}
		} // 再使用生成的 attrVals 数组进一步生成 SyscallData 结构体
		data.Calls = append(data.Calls, newSyscallData(target, c, attrVals[:last+1]))
		// Some syscalls might not be present on the compiling machine, so we
		// generate definitions for them.
		if target.SyscallNumbers && !strings.HasPrefix(c.CallName, "syz_") &&
			target.NeedSyscallDefine(c.NR) {
			defines[target.SyscallPrefix+c.CallName] = fmt.Sprintf("%d", c.NR)
		}
	}
	sort.Slice(data.Calls, func(i, j int) bool { // [3] 将生成的 data.Calls 数组进行排序,并返回 data 变量
		return data.Calls[i].Name < data.Calls[j].Name
	})
	// Get a sorted list of definitions.
	defineNames := []string{}
	for key := range defines {
		defineNames = append(defineNames, key)
	}
	sort.Strings(defineNames)
	for _, key := range defineNames {
		data.Defines = append(data.Defines, Define{key, defines[key]})
	}
	return data
}

​ reflect.ValueOf(c.Attrs) 在运行中获取c.Attrs 的值的意思嘛 反射

说明

  • [2] 作用,遍历各个 Syscall 类型的结构体, 将变量 c 中结构体 SyscallAttrs 里的各个字段取出,并将其依次存放至整型数组 attrVals (bool值和整型值);再使用生成的 attrVals 数组进一步生成 SyscallData 结构体

  • Syscall 结构体 -> SyscallAttrs 结构体

    type Syscall struct {
    	ID          int
    	NR          uint64 // kernel syscall number
    	Name        string
    	CallName    string
    	MissingArgs int // number of trailing args that should be zero-filled
    	Args        []Field
    	Ret         Type
    	Attrs       SyscallAttrs
      
    	inputResources  []*ResourceDesc
    	outputResources []*ResourceDesc
    }
    type SyscallAttrs struct {
    	Disabled      bool
    	Timeout       uint64
    	ProgTimeout   uint64
    	IgnoreReturn  bool
    	BreaksReturns bool
    }
    
  • data.CallsSyscallData 结构体示例与说明:

    [0]:<main.SyscallData>
      Name: "accept"
      CallName: "accept"
      NR: 30
      NeedCall: false
      
    // sys/syz-sysgen/sysgen.go
    type SyscallData struct {
        Name     string      // syzlang 中的调用名,例如 accept$inet
        CallName string      // 实际的 syscall 调用名,例如 accept
        NR       int32       // syscall 对应的调用号,例如 30
        NeedCall bool        // 一个用于后续的 syz-executor 源码生成的标志,后面会提到
        Attrs    []uint64    // 存放分析 syzlang 所生成的 SyscallAttrs 数据数组
    }
    

image-20230212182125986

writeExecutorSyscalls

功能:生成 syz-executor 所使用的 C 代码头文件写入 executor/defs.h ,将系统调用名和对应的系统调用号写入 executor\syscalls.h 文件。

func writeExecutorSyscalls(data *ExecutorData) {
	osutil.MkdirAll(filepath.Join(*outDir, "executor"))
	sort.Slice(data.OSes, func(i, j int) bool {
		return data.OSes[i].GOOS < data.OSes[j].GOOS
	})
	buf := new(bytes.Buffer) // [1] 生成 defs.h 文件
	if err := defsTempl.Execute(buf, data); err != nil {
		tool.Failf("failed to execute defs template: %v", err)
	}
	writeFile(filepath.Join(*outDir, "executor", "defs.h"), buf.Bytes())
	buf.Reset() // [2] 生成 syscalls.h 文件
	if err := syscallsTempl.Execute(buf, data); err != nil {
		tool.Failf("failed to execute syscalls template: %v", err)
	}
	writeFile(filepath.Join(*outDir, "executor", "syscalls.h"), buf.Bytes())
}

代码中提到 defsTempl 和 syscallsTempl模板如下

defsTempl 模板

说明:syz-sysgen 会将把先前 generateExecutorSyscalls 函数中所生成的 ArchData 结构体数据,导出至 executor/defs.h 文件中,供后续编译 syz-executor 所使用。syz-sysgen 将所有OS所有架构所对应的 ArchData 数据全部导出至一个文件中,并使用宏定义来选择启用哪一部分的数据。

模板如下:混杂着 C 宏定义与模板描述。

var defsTempl = template.Must(template.New("").Parse(`// AUTOGENERATED FILE

struct call_attrs_t { {{range $attr := $.CallAttrs}}
	uint64_t {{$attr}};{{end}}
};

struct call_props_t { {{range $attr := $.CallProps}}
	{{$attr.Type}} {{$attr.Name}};{{end}}
};

#define read_call_props_t(var, reader) { \{{range $attr := $.CallProps}}
	(var).{{$attr.Name}} = ({{$attr.Type}})(reader); \{{end}}
}

{{range $os := $.OSes}}
#if GOOS_{{$os.GOOS}}
#define GOOS "{{$os.GOOS}}"
{{range $arch := $os.Archs}}
#if GOARCH_{{$arch.GOARCH}}
#define GOARCH "{{.GOARCH}}"
#define SYZ_REVISION "{{.Revision}}"
#define SYZ_EXECUTOR_USES_FORK_SERVER {{.ForkServer}}
#define SYZ_EXECUTOR_USES_SHMEM {{.Shmem}}
#define SYZ_PAGE_SIZE {{.PageSize}}
#define SYZ_NUM_PAGES {{.NumPages}}
#define SYZ_DATA_OFFSET {{.DataOffset}}
{{range $c := $arch.Defines}}#ifndef {{$c.Name}}
#define {{$c.Name}} {{$c.Value}}
#endif
{{end}}#endif
{{end}}
#endif
{{end}}
`))

executor/defs.h 示例

// AUTOGENERATED FILE

struct call_attrs_t { 
    uint64_t disabled;
    uint64_t timeout;
    uint64_t prog_timeout;
    uint64_t ignore_return;
    uint64_t breaks_returns;
};

struct call_props_t { 
    int fail_nth;
};

#define read_call_props_t(var, reader) { \
    (var).fail_nth = (int)(reader); \
}


#if GOOS_akaros
#define GOOS "akaros"

#if GOARCH_amd64
#define GOARCH "amd64"
#define SYZ_REVISION "361c8bb8e04aa58189bcdd153dc08078d629c0b5"
#define SYZ_EXECUTOR_USES_FORK_SERVER 1
#define SYZ_EXECUTOR_USES_SHMEM 0
#define SYZ_PAGE_SIZE 4096
#define SYZ_NUM_PAGES 4096
#define SYZ_DATA_OFFSET 536870912
#endif

#endif

    ...
        
#if GOOS_linux
#define GOOS "linux"
   ...
#if GOARCH_amd64
#define GOARCH "amd64"
#define SYZ_REVISION "e61403f96ca19fc071d8e9c946b2259a2804c68e"
#define SYZ_EXECUTOR_USES_FORK_SERVER 1
#define SYZ_EXECUTOR_USES_SHMEM 1
#define SYZ_PAGE_SIZE 4096
#define SYZ_NUM_PAGES 4096
#define SYZ_DATA_OFFSET 536870912
#endif
    ...
#endif
    ...
        
#if GOOS_windows
#define GOOS "windows"

#if GOARCH_amd64
#define GOARCH "amd64"
#define SYZ_REVISION "8967babc353ed00daaa6992068d3044bad9d29fa"
#define SYZ_EXECUTOR_USES_FORK_SERVER 0
#define SYZ_EXECUTOR_USES_SHMEM 0
#define SYZ_PAGE_SIZE 4096
#define SYZ_NUM_PAGES 4096
#define SYZ_DATA_OFFSET 536870912
#endif

#endif

syscallsTempl 模板

说明executor/syscalls.h 下会存放着各个 syzlang 中所声明的 syscall 名与 syscall调用号的映射关系,以及可能有的 SyscallData。同时,也是使用宏定义来控制使用哪个OS哪个Arch下的 syscalls 映射关系

模板如下

// nolint: lll
var syscallsTempl = template.Must(template.New("").Parse(`// AUTOGENERATED FILE
// clang-format offz
{{range $os := $.OSes}}
#if GOOS_{{$os.GOOS}}
{{range $arch := $os.Archs}}
#if GOARCH_{{$arch.GOARCH}}
const call_t syscalls[] = {
{{range $c := $arch.Calls}}    {"{{$c.Name}}", {{$c.NR}}{{if or $c.Attrs $c.NeedCall}}, { {{- range $attr := $c.Attrs}}{{$attr}}, {{end}}}{{end}}{{if $c.NeedCall}}, (syscall_t){{$c.CallName}}{{end}}},
{{end}}};
#endif
{{end}}
#endif
{{end}}
`))

executor/syscalls.h 示例

...
#if GOOS_linux
...
#if GOARCH_amd64
const call_t syscalls[] = {
    {"accept", 43},
    {"accept$alg", 43},
    {"accept$ax25", 43},
    {"accept$inet", 43},
    {"accept$inet6", 43},
    {"accept$netrom", 43},
    {"accept$nfc_llcp", 43},
    ....,
    {"bind", 49},
    {"bind$802154_dgram", 49},
    {"bind$802154_raw", 49},
    {"bind$alg", 49},
    {"bind$ax25", 49},
    {"bind$bt_hci", 49},
    {"bind$bt_l2cap", 49},
    ....
    {"prctl$PR_CAPBSET_DROP", 167, {0, 0, 0, 1, 1, }},
    {"prctl$PR_CAPBSET_READ", 167, {0, 0, 0, 1, 1, }},
    {"prctl$PR_CAP_AMBIENT", 167, {0, 0, 0, 1, 1, }},
    ....
}
#endif
...
#endif
...

syscallData 结构体

type SyscallData struct {
    Name     string
    CallName string
    NR       int32
    NeedCall bool
    Attrs    []uint64
}

小结

当执行完 syz-extractor 为每个 syslang 文件生成一个常量映射表 .const 文件后,syz-sysgen 便会利用常量映射表,来彻底的解析 syzlang 源码,获取到其中声明的类型信息与 syscall 参数依赖关系。

当这些信息全都收集完毕后,syz-sysgen 便会将这些数据全部序列化为 go 文件,以供后续 syz-fuzzer 所使用。除此之外,syz-sysgen 还会创建 executor/defs.h 和 executor/syscalls.h,将部分信息导出至 C 头文件,以供后续 syz-executor 编译使用。

简单地说,syz-sysgen 解析 syzlang 文件,并为 syz-fuzzer 和 syz-executor 的编译运行做准备。

参考

https://kiprey.github.io/2022/03/syzkaller-1/

https://bsauce.github.io/2022/05/13/syzkaller1/
https://47.99.84.243/fuzz/Syzkaller%20executor%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/

https://github.com/google/syzkaller

syzkaller 源码阅读笔记-2

前言

我们上一篇分析了syz-extract 和 syz-sysgen

sysgen 用于解析syzlang,提取syscall 和参数

syz-extract 用于解析syzlang常量

本次主要分析syz-manager

syz-manager

syz-manager 的功能 是 负责 各种的启动,HTTP,RPC,dashboard,调用fuzz 以及repro(reproducible可重现性的测试结果)的生成

我们在开始的 时候都是

./syz-manager -config=4.14.cfg -vv 10

参数 我们可以见syzkaller安装的文章

{
    "target": "linux/amd64", //目标架构
    "http": "0.0.0.0:8080",//http的端口
    "workdir": "/home/test/go/src/github.com/google/syzkaller/bin/workdir",//syz-namager的工作目录
    "kernel_obj": "/home/test/桌面/cheche/kernel/linux-4.4.146/", //内核的目录,主要是去寻找vmlinux
    "image": "../image/stretch.img",//文件系统镜像
    "sshkey": "../image/stretch.id_rsa",//私钥
    "syzkaller": "/home/test/go/src/github.com/google/syzkaller",//syzkaller 的目录
    "disable_syscalls": ["keyctl", "add_key", "request_key"], // 禁用的系统调用列表
    "enable_syscalls": ["chmod"],//syzkaller使用的系统调用列表
    "suppressions": ["some known bug"],  // 已知错误的正则表达式列表
    "procs": 1,  // 每个VM中的并行测试进程数,一般是4或8
    "type": "qemu",// 要使用的虚拟机类型,例如qemu
    "vm": {// 特定VM类型相关的参数
        "count": 1,// 并行运行的VM数
        "kernel": "/home/test/桌面/cheche/kernel/linux-4.4.146/arch/x86/boot/bzImage",// 要测试的内核的bzImage文件的位置
        "cpu": 1,// 要在VM中模拟的CPU数
        "mem": 1024// VM的内存大小,以MB为单位
    }
}

有一些其他的参数

  • email_addrs:第一次出现bug时接收通知的电子邮件地址,只支持 Mailx
  • sshkey:用于与虚拟机通信的SSH密钥的位置
  • sandbox:沙盒模式,支持以下模式
    • none:默认设置,不做任何特殊的事情
    • setuid:冒充用户nobody(65534)
    • namespace:使用命名空间删除权限(内核需要设置 CONFIG_NAMESPACESCONFIG_UTS_NSCONFIG_USER_NSCONFIG_PID_NSCONFIG_NET_NS 构建)

debug 参数和 bench参数 debug参数将VM所有输出打印到console帮助我们排查使用中出现的错误;bench参数定期将执行的统计信息写入我们指定的文件。

var (
	flagConfig = flag.String("config", "", "configuration file")
	flagDebug  = flag.Bool("debug", false, "dump all VM output to console")
	flagBench  = flag.String("bench", "", "write execution statistics into this file periodically")
)

main

syz-manager/manager.go

功能:开启日志缓存,加载config文件,调用Runmananger

func main() {
	if prog.GitRevision == "" {
		log.Fatalf("bad syz-manager build: build with make, run bin/syz-manager")
	}
	flag.Parse()
	log.EnableLogCaching(1000, 1<<20)//开启日志缓存,提高性能,超过1000行会被覆盖,或者 1MB 超过丢弃
	cfg, err := mgrconfig.LoadFile(*flagConfig)//加载 config文件
	if err != nil {
		log.Fatalf("%v", err)
	}
	RunManager(cfg)//接着分析
}

RunManager(cfg)

功能:新开线程,定期记录VM状态,crash数量等信息,最后调用vmloop()

func RunManager(cfg *mgrconfig.Config) {
	var vmPool *vm.Pool
	// Type "none" is a special case for debugging/development when manager
	// does not start any VMs, but instead you start them manually
	// and start syz-fuzzer there.
	if cfg.Type != "none" {// 将type指定为none是在调试/开发中用的,这样manager就不会启动VM而是需要手动启动
		var err error
		vmPool, err = vm.Create(cfg, *flagDebug)//创建 vmPool,一个vmpool可用于创建多个独立的VM,vm.go 对不同的虚拟化方案提供了统一的接口,会调用qemu.go:Ctor 函数 主要检查一些参数
		if err != nil {
			log.Fatalf("%v", err)
		}
	}
//crashdir 很明白
	crashdir := filepath.Join(cfg.Workdir, "crashes")
	osutil.MkdirAll(crashdir)
//reporter导出
	reporter, err := report.NewReporter(cfg)
	if err != nil {
		log.Fatalf("%v", err)
	}

	mgr := &Manager{
		cfg:              cfg,
		vmPool:           vmPool,
		target:           cfg.Target,
		sysTarget:        cfg.SysTarget,
		reporter:         reporter,
		crashdir:         crashdir,
		startTime:        time.Now(),
		stats:            &Stats{haveHub: cfg.HubClient != ""},
		crashTypes:       make(map[string]bool),
		corpus:           make(map[string]CorpusItem),
		disabledHashes:   make(map[string]struct{}),
		memoryLeakFrames: make(map[string]bool),
		dataRaceFrames:   make(map[string]bool),
		fresh:            true,
		vmStop:           make(chan bool),
		hubReproQueue:    make(chan *Crash, 10),
		needMoreRepros:   make(chan chan bool),
		reproRequest:     make(chan chan map[string]bool),
		usedFiles:        make(map[string]time.Time),
		saturatedCalls:   make(map[string]bool),
	}

	mgr.preloadCorpus()
	mgr.initStats() // Initializes prometheus variables.
	mgr.initHTTP()  // Creates HTTP server.
	mgr.collectUsedFiles()

	// Create RPC server for fuzzers.
	mgr.serv, err = startRPCServer(mgr)
	if err != nil {
		log.Fatalf("failed to create rpc server: %v", err)
	}

	if cfg.DashboardAddr != "" {
		mgr.dash, err = dashapi.New(cfg.DashboardClient, cfg.DashboardAddr, cfg.DashboardKey)
		if err != nil {
			log.Fatalf("failed to create dashapi connection: %v", err)
		}
	}

	if !cfg.AssetStorage.IsEmpty() {
		mgr.assetStorage, err = asset.StorageFromConfig(cfg.AssetStorage, mgr.dash)
		if err != nil {
			log.Fatalf("failed to init asset storage: %v", err)
		}
	}

	go func() { // [2] 新开线程,定期记录VM状态、crash数量等信息
		for lastTime := time.Now(); ; {
			time.Sleep(10 * time.Second)
			now := time.Now()
			diff := now.Sub(lastTime)
			lastTime = now
			mgr.mu.Lock()
			if mgr.firstConnect.IsZero() {
				mgr.mu.Unlock()
				continue
			}
			mgr.fuzzingTime += diff * time.Duration(atomic.LoadUint32(&mgr.numFuzzing))
			executed := mgr.stats.execTotal.get()
			crashes := mgr.stats.crashes.get()
			corpusCover := mgr.stats.corpusCover.get()
			corpusSignal := mgr.stats.corpusSignal.get()
			maxSignal := mgr.stats.maxSignal.get()
			mgr.mu.Unlock()
			numReproducing := atomic.LoadUint32(&mgr.numReproducing)
			numFuzzing := atomic.LoadUint32(&mgr.numFuzzing)

			log.Logf(0, "VMs %v, executed %v, cover %v, signal %v/%v, crashes %v, repro %v",
				numFuzzing, executed, corpusCover, corpusSignal, maxSignal, crashes, numReproducing)
		}
	}()

	if *flagBench != "" {  //如果设置了 bench 参数,还要在指定的文件中记录一些信息。
		mgr.initBench()
	}

	if mgr.dash != nil {
		go mgr.dashboardReporter()
	}

	osutil.HandleInterrupts(vm.Shutdown)
	if mgr.vmPool == nil {
		log.Logf(0, "no VMs started (type=none)")
		log.Logf(0, "you are supposed to start syz-fuzzer manually as:")
		log.Logf(0, "syz-fuzzer -manager=manager.ip:%v [other flags as necessary]", mgr.serv.port)
		<-vm.Shutdown
		return
	}
	mgr.vmLoop()// [5] 主要调用 vmLoop()
}

  1. 根据传入的配置文件 cfg 创建一个虚拟机池 vmPool(如果配置类型不是 “none”)。
  2. 创建用于处理崩溃报告的 reporter,并创建与之关联的 崩溃存储目录 crashdir
  3. 创建 Manager 对象,并初始化其各个字段,包括统计信息、语料库等。
  4. 调用 preloadCorpus() 函数预加载语料库文件。
  5. 初始化 prometheus 变量和 HTTP 服务器。
  6. 创建用于处理 RPC 的服务器。
  7. 如果已指定仪表盘地址,则创建 dashapi 连接。
  8. 根据配置文件中的参数,初始化资产存储对象

vmLoop()

功能:

将VM实例分为两个部分,一部分用于进行crash复现,另一部分用于进行fuzz。

说明:

  • 变量说明:reproQueue —— 保存crash,可通过 len(reproQueue) != 0 判断当前是否有等待复现的crash;
  • [3]:可以复现且有剩余的 instances,则复现crash;
  • [4]:没有可复现的但是有剩余的 instances,则进行fuzz;
func (mgr *Manager) vmLoop() {
...
		canRepro := func() bool {  // [2] 判断当前是否有等待复现的crash
			return phase >= phaseTriagedHub && len(reproQueue) != 0 &&
				(int(atomic.LoadUint32(&mgr.numReproducing))+1)*instancesPerRepro <= maxReproVMs
		}

		if shutdown != nil {
			for canRepro() { // [3] 可以复现且有剩余的 instances, 则复现crash
				vmIndexes := instances.Take(instancesPerRepro)// [3-1] 取 instancesPerRepro 个 (默认4) VM, 对crash进行复现
				if vmIndexes == nil {
					break
				}
				last := len(reproQueue) - 1
				crash := reproQueue[last]
				reproQueue[last] = nil
				reproQueue = reproQueue[:last]
				atomic.AddUint32(&mgr.numReproducing, 1)
				log.Logf(1, "loop: starting repro of '%v' on instances %+v", crash.Title, vmIndexes)
				go func() {
					reproDone <- mgr.runRepro(crash, vmIndexes, instances.Put)// [3-2] crash 复现 runRepro() -> repro.Run() -> ctx.repro() !!!
				}()
			}
            
            
			for !canRepro() { // [4] 没有可复现的但是有剩余的 instances, 则进行fuzz
				idx := instances.TakeOne()// [4-1] 取 1 个 VM, 运行新的实例
				if idx == nil {
					break
				}
				log.Logf(1, "loop: starting instance %v", *idx)
				go func() {
					crash, err := mgr.runInstance(*idx)// [4-2] 启动fuzz, 监控信息并返回Report对象 runInstance() -> runInstanceInner() -> FuzzerCmd() & MonitorExecution()  !!!
					runDone <- &RunResult{*idx, crash, err}
				}()
			}
		}

		var stopRequest chan bool
		if !stopPending && canRepro() {
			stopRequest = mgr.vmStop
		}

    //下面的部分是主循环的无限循环体
    
	wait:
		select {
		case <-instances.Freed: //当读取到instances.Freed通道时,表示某个虚拟机已经空闲,可以用于下一个测试或重现任务
			// An instance has been released.
		case stopRequest <- true://当收到stopRequest通道中的信号时,表示需要停止所有正在进行的测试和重现,并尽快关闭所有虚拟机。
			log.Logf(1, "loop: issued stop request")
			stopPending = true
		case res := <-runDone://当从runDone通道中读取到一个结果时,表示某个测试或重现任务已经完成,该函数将更新其状态,并将虚拟机标记为空闲状态。
			log.Logf(1, "loop: instance %v finished, crash=%v", res.idx, res.crash != nil)
			if res.err != nil && shutdown != nil {
				log.Logf(0, "%v", res.err)
			}
			stopPending = false
			instances.Put(res.idx)
			// On shutdown qemu crashes with "qemu: terminating on signal 2",
			// which we detect as "lost connection". Don't save that as crash.
			if shutdown != nil && res.crash != nil {
				needRepro := mgr.saveCrash(res.crash)
				if needRepro {
					log.Logf(1, "loop: add pending repro for '%v'", res.crash.Title)
					pendingRepro[res.crash] = true
				}
			}
		case res := <-reproDone://当从reproDone通道中读取到一个结果时,表示某个崩溃的重现已经完成,该函数将更新其状态,并将重现结果保存到数据库中。
			atomic.AddUint32(&mgr.numReproducing, ^uint32(0))
			crepro := false
			title := ""
			if res.repro != nil {
				crepro = res.repro.CRepro
				title = res.repro.Report.Title
			}
			log.Logf(1, "loop: repro on %+v finished '%v', repro=%v crepro=%v desc='%v'",
				res.instances, res.report0.Title, res.repro != nil, crepro, title)
			if res.err != nil {
				log.Logf(0, "repro failed: %v", res.err)
			}
			delete(reproducing, res.report0.Title)
			if res.repro == nil {
				if !res.hub {
					mgr.saveFailedRepro(res.report0, res.stats)
				}
			} else {
				mgr.saveRepro(res)
			}
		case <-shutdown://当从shutdown通道中读取到一个信号时,表示需要关闭所有虚拟机并结束程序执行。
			log.Logf(1, "loop: shutting down...")
			shutdown = nil
		case crash := <-mgr.hubReproQueue://当从mgr.hubReproQueue通道中读取到一个新的崩溃时,表示外部系统(例如fuzzing hub)发送了一个可重现的崩溃,该函数将把它添加到重现队列中。
			log.Logf(1, "loop: get repro from hub")
			pendingRepro[crash] = true
		case reply := <-mgr.needMoreRepros://当从mgr.needMoreRepros通道中读取到一个请求时,表示需要检查是否还有待处理的崩溃。如果所有崩溃都已处理完毕,则回复true;否则回复false。
			reply <- phase >= phaseTriagedHub &&
				len(reproQueue)+len(pendingRepro)+len(reproducing) == 0
			goto wait
		case reply := <-mgr.reproRequest://当从mgr.reproRequest通道中读取到一个请求时,表示需要返回正在进行的重现列表。该函数将获取所有正在进行的重现,并将它们的标题保存在repros映射中返回。
			repros := make(map[string]bool)
			for title := range reproducing {
				repros[title] = true
			}
			reply <- repros
			goto wait
		}
	}
}

该函数是一个无限循环,是整个系统的核心。它使用了多种并发和同步机制,并通过监听各种事件和状态变化来调度虚拟机运行和崩溃重现。以下是对该函数的详细介绍:

  1. 首先,该函数使用一个资源池(instances)来管理可用实例的索引号码。资源池存储在结构体Manager中,并在函数开头初始化。资源池可以检测已释放的实例并通知正在等待实例的goroutine。
  2. 接着,该函数根据配置设置每个崩溃需要重现的虚拟机数量,以及可以使用的最大虚拟机数。因此,在代码中,有两个变量instancesPerRepro和maxReproVMs,其值都与可用的虚拟机数有关。如果实际可用的虚拟机数小于要求,则会相应地更改instancesPerRepro的值。
  3. 为了管理虚拟机的运行和崩溃的重现,该函数创建了三个通道:runDone、reproDone和hubReproQueue。这些通道分别用于处理运行结果、重现结果和从集线器接收重现请求。当处理结果时,该函数会根据需要保存崩溃并加入重现队列等待重现。
  4. 在函数的主循环中,它会不断检查当前状态,并根据需要调整虚拟机的运行和崩溃的重现。该函数使用多个映射,包括pendingRepro、reproducing和reproQueue,来跟踪正在处理的重现。如果发现新的重现请求,则将其添加到pendingRepro中。
  5. 如果当前还有空闲实例,则会启动实例以进行测试,并异步地等待运行结果。如果存在可以使用的虚拟机并且重现队列不为空,则会开始重现流程。在启动重现之前,该函数会检查当前是否已经有足够数量的虚拟机正在工作(instancesPerRepro决定),以免占用太多资源。
  6. 为了更好地控制整个系统的状态,该函数使用许多事件通道来等待相应的事件发生,例如资源池中某个实例被释放、需要停止正在执行的测试或重现以及从集线器接收重现请求等。在监听这些事件时,它使用goto语句来重新进入等待状态,直到特定的事件发生并满足某些条件时再继续执行。
crash 复现

调用链vmLoop() -> mgr.runRepro() -> repro.Run() -> ctx.repro() (重点函数)

位置pkg/repro/repro.go: (*context).repro()

功能:crash 复现,提取出触发crash的C代码。

runRepro 测试崩溃,调用repro.run

func (mgr *Manager) runRepro(crash *Crash, vmIndexes []int, putInstances func(...int)) *ReproResult {
	features := mgr.checkResult.Features
	res, stats, err := repro.Run(crash.Output, mgr.cfg, features, mgr.reporter, mgr.vmPool, vmIndexes)
...

pkg/repro/repro.go Run

	func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, reporter *report.Reporter,
	vmPool *vm.Pool, vmIndexes []int) (*Result, *Stats, error) {
	....
	res, err := ctx.repro(entries, crashStart)
	if err != nil {
		return nil, nil, err
	}
	if res != nil {
		ctx.reproLogf(3, "repro crashed as (corrupted=%v):\n%s",
			ctx.report.Corrupted, ctx.report.Report)
		// Try to rerun the repro if the report is corrupted.
		for attempts := 0; ctx.report.Corrupted && attempts < 3; attempts++ {
			ctx.reproLogf(3, "report is corrupted, running repro again")
			if res.CRepro {
				_, err = ctx.testCProg(res.Prog, res.Duration, res.Opts)
			} else {
				_, err = ctx.testProg(res.Prog, res.Duration, res.Opts)
			}
			if err != nil {
				return nil, nil, err
			}
		}
		ctx.reproLogf(3, "final repro crashed as (corrupted=%v):\n%s",
			ctx.report.Corrupted, ctx.report.Report)
		res.Report = ctx.report
	}
	return res, ctx.stats, nil
}
这部分代码是程序中的一个函数 Run,它会运行 fuzzing 过程并尝试重现崩溃。具体来说:

首先,该函数会调用上下文对象的 repro 方法来尝试重现崩溃,并将得到的结果保存到 res 变量中。

如果重现成功,则 res 将包含相关的信息,例如重现用时、产生崩溃的程序等。

如果重现失败,或者重现后报告被标记为 "corrupted",则该函数将尝试重新运行重现多达三次,以确保报告数据正确。

最终,该函数将返回重现结果 res、统计数据 ctx.stats 和可能出现的错误。
ctx.repro这个函数

repro函数

  • [2] ctx.extractProg() —— 提取出触发 crash 的程序;
  • [3] ctx.minimizeProg() —— 若成功复现,则调用 prog.Minimize(),简化所有的调用和参数;
  • [4] ctx.extractC() —— 生成C代码,编译成二进制文件,执行并检查是否crash;
  • [5] ctx.simplifyProg() —— 进一步简化。在 repro.go 中定义了 progSimplifies 数组作为简化规则,依次使用每一条规则后,如果crash还能被触发, 再调用 extractC(res) 尝试提取 C repro;
  • [6] ctx.simplifyC() —— 对提取出的C程序进行简化。 跟上面的 ctx.simplifyProg(res) 差不多,就是规则使用了 cSimplifies 数组;
  • [5][6] 简化的是复现crash时设置的一些选项,比如线程、并发、沙盒等等。简化选项分别保存在 progSimplifiescSimplifies 数组中。
func (ctx *context) repro(entries []*prog.LogEntry, crashStart int) (*Result, error) {
	// Cut programs that were executed after crash.
	for i, ent := range entries {
		if ent.Start > crashStart {
			entries = entries[:i]
			break
		}
	}

	reproStart := time.Now()
	defer func() {
		ctx.reproLogf(3, "reproducing took %s", time.Since(reproStart))
	}()

	res, err := ctx.extractProg(entries)// [2] 提取出触发 crash 的程序  !!!
	if err != nil {
		return nil, err
	}
	if res == nil {
		return nil, nil
	}
	defer func() {
		if res != nil {
			res.Opts.Repro = false
		}
	}()
	res, err = ctx.minimizeProg(res) // [3] 若成功复现, 则调用prog.Minimize(), 简化所有的调用和参数 !!!
	if err != nil {
		return nil, err
	}

	// Try extracting C repro without simplifying options first.
	res, err = ctx.extractC(res)// [4] 生成C代码,编译成二进制文件,执行并检查是否crash,若crash则赋值 res.CRepro = crashed !!!
	if err != nil {
		return nil, err
	}

	// Simplify options and try extracting C repro.
	if !res.CRepro {
		res, err = ctx.simplifyProg(res)//[5] !!! 进一步简化。在 repro.go 中定义了 progSimplifies 数组作为简化规则,依次使用每一条规则后,如果crash还能被触发, 再调用 extractC(res) 尝试提取 C repro
		if err != nil {
			return nil, err
		}
	}

	// Simplify C related options.
	if res.CRepro {
		res, err = ctx.simplifyC(res)// [6] 对提取出的C程序进行简化。 跟上面的ctx.simplifyProg(res)差不多,就是规则使用了cSimplifies数组。[5][6] 简化的是复现crash时设置的一些选项,比如线程、并发、沙盒等等。
		if err != nil {
			return nil, err
		}
	}

	return res, nil
}

这个函数是 fuzzing 过程中重现崩溃的核心部分,它会尝试从记录的日志中提取触发崩溃的程序,并进行简化以便更容易重现崩溃。具体来说:

  1. 首先,该函数会根据记录的日志,找到触发崩溃的程序所在的 entries,并将后续的所有执行记录截断。
  2. 然后,该函数调用 extractProg 方法来提取出触发崩溃的程序,并将结果保存到 res 变量中。
  3. 如果成功提取出程序,则继续调用 minimizeProg 方法对其进行简化,以便更容易重现崩溃。
  4. 接下来,该函数会尝试提取 C 语言的重现程序,不论之前是否已经有过简化操作。
  5. 如果之前提取出的程序不是 C 语言程序,则调用 simplifyProg 方法对选项进行简化。
  6. 如果程序是 C 语言程序,则调用 simplifyC 方法对 C 相关的选项进行简化。
  7. 最后,函数返回重现结果 res,以及可能出现的错误。
extractProg()

位置pkg/repro/repro.go

功能:提取出触发 crash 的程序。

说明:按照时间从短到长, 从后向前, 从单个到多个的顺序复现crash。

  • [1]:在所有程序 (用 entries 数组存放) 中提取出每个proc所执行的最后一个程序;
  • [2]:将程序按倒序存放到 lastEntries (通常最后一个程序就是触发crash的程序);
  • [3]:不同类型的漏洞漏洞需要不同的复现时间, 复杂crash耗时长(eg, race);
  • [4] extractProgSingle() —— 倒序执行单个程序, 若触发crash则返回;
  • [5] extractProgBisect() —— 若单个程序无法触发crash, 则采用二分查找的方法找出哪几个程序一起触发crash。先调用 bisectProgs() 进行分组,看哪一组可以触发crash。 !!!
  • 返回值是能触发crash的单个program或者能触发crash的programs的组合。
func (ctx *context) extractProg(entries []*prog.LogEntry) (*Result, error) {
	ctx.reproLogf(2, "extracting reproducer from %v programs", len(entries))
	start := time.Now()
	defer func() {
		ctx.stats.ExtractProgTime = time.Since(start)
	}()

	// Extract last program on every proc.
	procs := make(map[int]int)
	for i, ent := range entries {
		procs[ent.Proc] = i
	}
	var indices []int
	for _, idx := range procs {// [1] 在所有程序 (用entries数组存放) 中提取出每个proc所执行的最后一个程序
		indices = append(indices, idx)
	}
	sort.Ints(indices)
	var lastEntries []*prog.LogEntry
	for i := len(indices) - 1; i >= 0; i-- {// [2] 将程序按倒序存放到 lastEntries (通常最后一个程序就是触发crash的程序)
		lastEntries = append(lastEntries, entries[indices[i]])
	}
	for _, timeout := range ctx.testTimeouts {// [3] 不同类型的漏洞漏洞需要不同的复现时间, 复杂crash耗时长(eg, race)
		// Execute each program separately to detect simple crashes caused by a single program.
		// Programs are executed in reverse order, usually the last program is the guilty one.
		res, err := ctx.extractProgSingle(lastEntries, timeout)// [4] 倒序执行单个程序, 若触发crash则返回
		if err != nil {
			return nil, err
		}
		if res != nil {
			ctx.reproLogf(3, "found reproducer with %d syscalls", len(res.Prog.Calls))
			return res, nil
		}

		// Don't try bisecting if there's only one entry.
		if len(entries) == 1 {
			continue
		}
 // [5] 若单个程序无法触发crash, 则采用二分查找的方法找出哪几个程序一起触发crash。先调用bisectProgs()进行分组,看哪一组可以触发crash。 !!!
		// Execute all programs and bisect the log to find multiple guilty programs.
		res, err = ctx.extractProgBisect(entries, timeout)
		if err != nil {
			return nil, err
		}
		if res != nil {
			ctx.reproLogf(3, "found reproducer with %d syscalls", len(res.Prog.Calls))
			return res, nil
		}
	}

	ctx.reproLogf(0, "failed to extract reproducer")
	return nil, nil
}

这个函数是 repro 方法的一部分,它的作用是从执行日志中提取出触发崩溃的程序。具体来说:

  1. 首先,该函数会对所有执行记录进行处理,找到每个进程(proc)所执行的最后一个程序,并将这些程序保存到 lastEntries 变量中。
  2. 接下来,该函数会尝试按照一定顺序执行 lastEntries 中的每个程序,并检查是否有任何简单的崩溃。如果找到了崩溃程序,则返回包含相应信息的 Result 对象。
  3. 如果没有找到崩溃程序,且执行记录 entries 中包含多个程序,则尝试执行所有程序,并使用二分法(bisect)来检查哪些程序可能导致崩溃。如果找到崩溃程序,则同样返回相应信息的 Result 对象。
  4. 最后,如果无论如何都无法找到崩溃程序,则返回 nil

需要注意的是,该函数在具体实现上使用了许多其他辅助函数和数据类型,如 extractProgSingleextractProgBisect 等。

Minimize()

调用链ctx.minimizeProg() -> prog.Minimize()(重点函数)

位置prog/minimization.go: Minimize()

功能:简化所有的调用和参数。

说明

  • [1] sanitizeFix() —— 有些系统调用需要做一些特殊的处理;
  • [2] removeCalls() —— 尝试逐个移除系统调用;
  • [3] :去除系统调用的无关参数;
  • [4] ctx.do() —— 根据不同的参数类型调用不同的minimize函数
    • func (typ *PtrType) minimize() —— 如果参数是指针类型的,把指针或者指针指向的内容置空;
    • func (typ *ArrayType) minimize() —— 如果参数是数组类型的,尝试一个一个移除数组中的元素;
// Minimize calls and arguments.
func (ctx *context) minimizeProg(res *Result) (*Result, error) {

	res.Prog, _ = prog.Minimize(res.Prog, -1, true,

调用Minimize 也就是 prog/minimization.go: Minimize()

// Minimize minimizes program p into an equivalent program using the equivalence
// predicate pred. It iteratively generates simpler programs and asks pred
// whether it is equal to the original program or not. If it is equivalent then
// the simplification attempt is committed and the process continues.
func Minimize(p0 *Prog, callIndex0 int, crash bool, pred0 func(*Prog, int) bool) (*Prog, int) {
	pred := func(p *Prog, callIndex int) bool {
		p.sanitizeFix()// [1] 有些系统调用需要做一些特殊的处理 !!!
		p.debugValidate()
		return pred0(p, callIndex)
	}
	name0 := ""
	if callIndex0 != -1 {
		if callIndex0 < 0 || callIndex0 >= len(p0.Calls) {
			panic("bad call index")
		}
		name0 = p0.Calls[callIndex0].Meta.Name
	}

	// Try to remove all calls except the last one one-by-one.
	p0, callIndex0 = removeCalls(p0, callIndex0, crash, pred)// [2] 尝试逐个移除系统调用

	// Try to reset all call props to their default values.
	p0 = resetCallProps(p0, callIndex0, pred)

	// Try to minimize individual calls.
	for i := 0; i < len(p0.Calls); i++ {// [3] 去除系统调用的无关参数
		if p0.Calls[i].Meta.Attrs.NoMinimize {
			continue
		}
		ctx := &minimizeArgsCtx{
			target:     p0.Target,
			p0:         &p0,
			callIndex0: callIndex0,
			crash:      crash,
			pred:       pred,
			triedPaths: make(map[string]bool),
		}
	again:
		ctx.p = p0.Clone()
		ctx.call = ctx.p.Calls[i]
		for j, field := range ctx.call.Meta.Args {
			if ctx.do(ctx.call.Args[j], field.Name, "") {// [4] 在do函数中,根据不同的参数类型调用不同的minimize函数 !!!
				goto again
			}
		}
		p0 = minimizeCallProps(p0, i, callIndex0, pred)
	}

	if callIndex0 != -1 {
		if callIndex0 < 0 || callIndex0 >= len(p0.Calls) || name0 != p0.Calls[callIndex0].Meta.Name {
			panic(fmt.Sprintf("bad call index after minimization: ncalls=%v index=%v call=%v/%v",
				len(p0.Calls), callIndex0, name0, p0.Calls[callIndex0].Meta.Name))
		}
	}
	return p0, callIndex0
}

这个函数实现了程序的最小化,旨在将一个程序简化为与其等价的最小形式。具体来说:

  1. 首先,该函数尝试逐个删除所有调用,直到只剩下最后一个调用。在每次删除前,调用 pred 判断是否仍然等效,如果是,则继续删除操作。
  2. 接下来,该函数尝试将所有调用的属性重置为默认值,并再次调用 pred 判断是否仍然等效。
  3. 最后,该函数对每个调用进行单独的最小化操作。对于每个调用,它会将参数传递给 do 方法,并生成新的程序。如果 pred 返回 true,则表示新程序仍然等效,此时重复上述过程,直到不能再进一步简化为止。

需要注意的是,该函数在具体实现上使用了许多其他辅助函数和数据类型,如 removeCallsresetCallProps 等。

extractC()

调用链ctx.extractC() -> ctx.testCProg() -> inst.RunCProg() -> csource.Write() & csource.BuildNoWarn() & inst.runBinary()

位置pkg/instance/execprog.go: (*ExecProgInstance).RunCProg()

功能:生成C代码,编译成二进制文件,执行并检查是否crash。

说明:调用 csource.Write() 生成C代码; csource.BuildNoWarn() 编译出可执行文件; inst.runBinary() 执行二进制文件。

func (inst *ExecProgInstance) RunCProg(p *prog.Prog, duration time.Duration,
	opts csource.Options) (*RunResult, error) {
	src, err := csource.Write(p, opts)
	if err != nil {
		return nil, err
	}
	inst.Logf(2, "testing compiled C program (duration=%v, %+v): %s", duration, opts, p)
	return inst.RunCProgRaw(src, p.Target, duration)
}

func (inst *ExecProgInstance) RunCProgRaw(src []byte, target *prog.Target,
	duration time.Duration) (*RunResult, error) {
	bin, err := csource.BuildNoWarn(target, src)
	if err != nil {
		return nil, err
	}
	defer os.Remove(bin)
	return inst.runBinary(bin, duration)

这两个函数是 ExecProgInstance 结构体的方法,用于执行经过编译的 C 程序。具体来说:

  1. RunCProg 方法接收一个 Go 语言的程序 p,先将其转换为 C 语言源代码,并使用 csource.Options 对象指定编译选项,最后调用 RunCProgRaw 方法执行编译好的二进制文件。
  2. RunCProgRaw 方法接收一个二进制文件(已经通过 csource.BuildNoWarn 编译好了),并在执行过程中限制运行时间不超过 duration。如果运行成功,则返回包含相应信息的 RunResult 对象,否则返回错误信息。

需要注意的是,这两个方法都使用了 os.Remove 函数删除了生成的临时文件

simplifyProg()

调用链: simplifyProg() -> testProg() -> testProgs ->testWithInstance & RunSyzProg

位置:

// Simplify repro options (threaded, sandbox, etc).
func (ctx *context) simplifyProg(res *Result) (*Result, error) {
	ctx.reproLogf(2, "simplifying guilty program options")
	start := time.Now()
	defer func() {
		ctx.stats.SimplifyProgTime = time.Since(start)
	}()

	// Do further simplifications.
	for _, simplify := range progSimplifies {
		opts := res.Opts
		if !simplify(&opts) || !checkOpts(&opts, ctx.timeouts, res.Duration) {
			continue
		}
		crashed, err := ctx.testProg(res.Prog, res.Duration, opts)
		if err != nil {
			return nil, err
		}
		if !crashed {
			continue
		}
		res.Opts = opts
		// Simplification successful, try extracting C repro.
		res, err = ctx.extractC(res)
		if err != nil {
			return nil, err
		}
		if res.CRepro {
			return res, nil
		}
	}

	return res, nil
}

这个函数实现了对程序参数的简化,以便更容易地重现导致崩溃或错误的场景。具体来说:

  1. 在执行过程中,该函数首先遍历 progSimplifies 列表中的每个函数,并尝试将结果应用于 Result 对象的选项。
  2. 如果简化后的选项合法,则使用 ctx.testProg 函数测试是否可以在新的选项下触发相同的错误。如果测试成功,则将简化后的选项保存到 res.Opts 中,并进一步尝试提取 C 语言重现文件。
  3. 如果最终提取出 C 重现文件成功,则返回包含此信息的 Result 对象;否则,该函数将继续尝试其他简化方法,直到所有方法都被尝试完毕为止。

需要注意的是,在执行过程中,该函数会记录运行时间、统计信息等,并在最后返回处理后的 Result 对象。

testProgs

func (ctx *context) testProgs(entries []*prog.LogEntry, duration time.Duration, opts csource.Options) (
	crashed bool, err error) {
	if len(entries) == 0 {
		return false, fmt.Errorf("no programs to execute")
	}
	pstr := encodeEntries(entries)
	program := entries[0].P.String()
	if len(entries) > 1 {
		program = "["
		for i, entry := range entries {
			program += fmt.Sprintf("%v", len(entry.P.Calls))
			if i != len(entries)-1 {
				program += ", "
			}
		}
		program += "]"
	}
	ctx.reproLogf(2, "testing program (duration=%v, %+v): %s", duration, opts, program)
	ctx.reproLogf(3, "detailed listing:\n%s", pstr)
	//重点在下面
	return ctx.testWithInstance(func(inst *instance.ExecProgInstance) (*instance.RunResult, error) {
		return inst.RunSyzProg(pstr, duration, opts)
	})
}

这个函数实际上是一个调用 ctx.testWithInstance 函数的简写方式,其中传递了一个匿名函数作为参数。具体来说:

  1. 匿名函数的定义与类型为 func(*instance.ExecProgInstance) (*instance.RunResult, error),即它接收一个 *instance.ExecProgInstance 类型的参数,返回一个 *instance.RunResult 类型的对象以及一个错误对象(如果有)。
  2. 在执行过程中,该匿名函数将调用 inst.RunSyzProg 方法来执行 Syzkaller 程序,并返回相应的结果和错误值。
  3. 该函数将该匿名函数作为参数传递给 ctx.testWithInstance 函数,然后将其返回值(即 RunResult 对象和错误值)返回给调用者。

需要注意的是,ctx.testWithInstance 函数用于提供一个执行环境来运行程序,并在整个测试过程中跟踪统计信息。因此,这个函数的调用依赖于是否正确设置了 context 对象。

启动Fuzz

调用链vmLoop() -> mgr.runInstance() -> mgr.runInstanceInner()

位置syz-manager/manager.go: (*Manager).runInstanceInner()

功能:负责启动 syz-fuzzer

说明

  • [1]:将 syz-fuzzer 复制到VM中;
  • [2]:将 syz-executor 复制到VM中;
  • [3] FuzzerCmd() —— 构造好命令,通过ssh执行 syz-fuzzer
# fuzz命令示例
/syz-fuzzer -executor=/syz-executor -name=vm-0 -arch=amd64 -manager=10.0.2.10:33185 -procs=1 -leak=false -cover=true -sandbox=none -debug=true -v=100
  • [4] MonitorExecution() —— 监控, 检测输出中的内核oops信息、丢失连接、挂起等等。
func (mgr *Manager) runInstanceInner(index int, instanceName string) (*report.Report, []byte, error) {
	inst, err := mgr.vmPool.Create(index)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create instance: %v", err)
	}
	defer inst.Close()

	fwdAddr, err := inst.Forward(mgr.serv.port)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to setup port forwarding: %v", err)
	}

	fuzzerBin, err := inst.Copy(mgr.cfg.FuzzerBin)// [1] 将 syz-fuzzer 复制到VM中
	if err != nil {
		return nil, nil, fmt.Errorf("failed to copy binary: %v", err)
	}

	// If ExecutorBin is provided, it means that syz-executor is already in the image,
	// so no need to copy it.
	executorBin := mgr.sysTarget.ExecutorBin
	if executorBin == "" {
		executorBin, err = inst.Copy(mgr.cfg.ExecutorBin)// [2] 将 syz-executor 复制到VM中
		if err != nil {
			return nil, nil, fmt.Errorf("failed to copy binary: %v", err)
		}
	}

	fuzzerV := 0
	procs := mgr.cfg.Procs
	if *flagDebug {
		fuzzerV = 100
		procs = 1
	}

	// Run the fuzzer binary.
	start := time.Now()
	atomic.AddUint32(&mgr.numFuzzing, 1)
	defer atomic.AddUint32(&mgr.numFuzzing, ^uint32(0))

	args := &instance.FuzzerCmdArgs{
		Fuzzer:    fuzzerBin,
		Executor:  executorBin,
		Name:      instanceName,
		OS:        mgr.cfg.TargetOS,
		Arch:      mgr.cfg.TargetArch,
		FwdAddr:   fwdAddr,
		Sandbox:   mgr.cfg.Sandbox,
		Procs:     procs,
		Verbosity: fuzzerV,
		Cover:     mgr.cfg.Cover,
		Debug:     *flagDebug,
		Test:      false,
		Runtest:   false,
		Optional: &instance.OptionalFuzzerArgs{
			Slowdown:   mgr.cfg.Timeouts.Slowdown,
			RawCover:   mgr.cfg.RawCover,
			SandboxArg: mgr.cfg.SandboxArg,
		},
	}
	cmd := instance.FuzzerCmd(args)// [3] 调用 FuzzerCmd() 通过ssh执行 syz-fuzzer   !!!
	outc, errc, err := inst.Run(mgr.cfg.Timeouts.VMRunningTime, mgr.vmStop, cmd)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to run fuzzer: %v", err)
	}

	var vmInfo []byte
	rep := inst.MonitorExecution(outc, errc, mgr.reporter, vm.ExitTimeout)// [4] 监控, 检测输出中的内核oops信息、丢失连接、挂起等等。
	if rep == nil {
		// This is the only "OK" outcome.
		log.Logf(0, "%s: running for %v, restarting", instanceName, time.Since(start))
	} else {
		vmInfo, err = inst.Info()
		if err != nil {
			vmInfo = []byte(fmt.Sprintf("error getting VM info: %v\n", err))
		}
	}

	return rep, vmInfo, nil
}

参考

https://bsauce.github.io/2022/05/14/syzkaller2/

https://xz.aliyun.com/t/5154

https://bbs.kanxue.com/thread-268152.htm

https://github.com/google/syzkaller

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐