Syzkaller 源码分析1:syz-manager
yuyutoo 2024-10-22 18:42 1 浏览 0 评论
syzkaller 是由 Google 开发的一个十分强大的针对内核的 fuzzer,自其面世以来已经帮助全世界的内核安全研究员发现了数量惊人的内核漏洞
为了深入学习 fuzzing theory,笔者决定先从这个非常经典的 kernel fuzzer 的源码进行分析学习 :)
PRE.工作原理
对于 syzkaller 的架构,官方给出了这样的一张 Overview
syzkaller 整体上为一个双机调试结构:由一台机器负责管控整个 fuzzing 流程(本文称为 Host),在另一台机器上进行 fuzzing(本文称为 Guest),Guest 通常为虚拟机,从而能让 Host 更好地管控整个流程
syzkaller 分为三大组件:
- 位于 Host:syz-manager :syzkaller 的控制中枢,其会启动多个 VM 实例(如图所示的一个黄色卡片就是一个实例)并进行监视,同时通过 RPC 来启动 syz-fuzzer
- 位于 Guest:
- syz-fuzzer :负责引导整个 fuzz 的过程:生成 input启动 syz-executor 进程进行 fuzz从被 fuzz 的 kernel 的 /sys/kernel/debug/kcov 获得覆盖(coverage)的相关信息通过 RPC 将新的覆盖回送到 syz-manager
- syz-executor:负责执行单个输入——从 syz-fuzzer 处接受 input 并执行,最后回送结果
syz-manager 为 syzkaller 的控制中枢,其会启动多个 VM 实例并进行监视,同时通过 RPC 来启动 syz-fuzzer,我们通常启动 fuzzing 时便是以 syz-manager 作为程序启动的入口点,因此笔者也先从此处开始分析
相比于直接开始分析源码,笔者认为有必要在此之前先列出一些基本的结构体,你也可以把这一节当成一个表来查 :)
VM 管控相关
Host 需要去感知与管控 Guest VMs,因而在 syz-manager 当中有着一套相应的表示与管理 Guest VM 的结构体
1. Instance:VM 实例
syz-manager 中的 VM 实际上是使用一个名为 Instance 的结构体来表示的,定义于 vm/vm.go 中:
type Instance struct {
impl vmimpl.Instance
workdir string
timeouts targets.Timeouts
index int
onClose func()
}
类似地,其需要实现 Interface 接口,定义于 vm/vmimpl/vmimpl.go 中:
// Instance 表示一个单独的 VM.
type Instance interface {
// Copy 复制一个 hostSrc 文件到 VM 中并返回 VM 中的文件名.
Copy(hostSrc string) (string, error)
// Forward 设置从虚拟机内到主机上给定 tcp 端口的转发,
// 并返回要在虚拟机中使用的地址.
Forward(port int) (string, error)
// Run 在虚拟机内执行命令 (类似 ssh cmd).
// outc 接受混合了命令行与内核控制台的输出.
// errc 接受命令等待返回 error 或 vmimpl.ErrTimeout.
// Command 在 timeout 后停止. 在 stop chan 上发送可以用以更早将其终止.
Run(timeout time.Duration, stop <-chan bool, command string) (outc <-chan []byte, errc <-chan error, err error)
// Diagnose 从 VM 上检索额外的调试信息
// (例如通过发送一些 sys-rq's 或 SIGABORT'ing 一个 Go 程序).
//
// 选择性地直接返回 (一些或所有) 信息. 若 wait == true,
// 调用者必须等待 VM 直接输出信息到其日志.
//
// rep 描述了 Diagnose 被调用的原因.
Diagnose(rep *report.Report) (diagnosis []byte, wait bool)
// Close 停止并销毁 VM.
Close()
}
- Copy():将一个来自宿主机的文件拷贝至虚拟机中,返回虚拟机中的文件名.
- Forward():设置从虚拟机内到主机上给定 tcp 端口的转发,并返回要在虚拟机中使用的地址
- Run():在虚拟机内执行命令
- Diagnose():在虚拟机上检索额外的调试信息
- Close():停止并销毁虚拟机
需要注意的是不同类型的 Guest VM 所实现的 Interface 接口是不同的
以 QEMU 为例,其实现主要位于 vm/qemu/qemu.go 中
2.Pool:VM 池
类似于线程池的概念,在 syz-manager 中使用一个 VM 池 —— Pool 结构体来管控 Guest VM,该结构体定义于 vm/vm.go 中:
type Pool struct {
impl vmimpl.Pool
workdir string
template string
timeouts targets.Timeouts
activeCount int32
}
该结构体实现了 Pool 接口,定义于 vm/vmimpl/vmimpl.go 中:
// Pool 表示了一组特定类型的测试机器 (虚拟机, 物理设备, etc).
type Pool interface {
// Count 返回池中所有 VM 的数量.
Count() int
// Create 创建并启动一个新的 VM 实例.
Create(workdir string, index int) (Instance, error)
}
- Count():返回池中所有 VM 的数量
- Create():新建并启动一个 VM实例,返回新建的实例对象
QEMU VM 浅析
以 QEMU 为例的 Pool 接口实现如下,对于 Count() 而言会直接返回配置文件中的计数:
func (pool *Pool) Count() int {
return pool.cfg.Count
}
Create() 则会首先检查文件系统镜像是否为 9p 格式,若是则会生成一个 ssh key 存放到 key 文件中并生成一个 init.sh 文件;接下来就是调用 ctor() 函数创建虚拟机:
func (pool *Pool) Create(workdir string, index int) (vmimpl.Instance, error) {
sshkey := pool.env.SSHKey
sshuser := pool.env.SSHUser
if pool.env.Image == "9p" {
sshkey = filepath.Join(workdir, "key")
sshuser = "root"
if _, err := osutil.RunCmd(10*time.Minute, "", "ssh-keygen", "-t", "rsa", "-b", "2048",
"-N", "", "-C", "", "-f", sshkey); err != nil {
return nil, err
}
initFile := filepath.Join(workdir, "init.sh")
if err := osutil.WriteExecFile(initFile, []byte(strings.Replace(initScript, "{{KEY}}", sshkey, -1))); err != nil {
return nil, fmt.Errorf("failed to create init file: %v", err)
}
}
for i := 0; ; i++ {
inst, err := pool.ctor(workdir, sshkey, sshuser, index)
if err == nil {
return inst, nil
}
// Older qemu prints "could", newer -- "Could".
if i < 1000 && strings.Contains(err.Error(), "ould not set up host forwarding rule") {
continue
}
if i < 1000 && strings.Contains(err.Error(), "Device or resource busy") {
continue
}
return nil, err
}
}
ctor() 的实现比较简单,主要就是创建一个带着 ssh key 及一些配置信息与一个 channel 的 instance 实例,初始化实例内的管道并调用 boot() 函数进行正式的创建:
func (pool *Pool) ctor(workdir, sshkey, sshuser string, index int) (vmimpl.Instance, error) {
inst := &instance{
index: index,
cfg: pool.cfg,
target: pool.target,
archConfig: pool.archConfig,
version: pool.version,
image: pool.env.Image,
debug: pool.env.Debug,
os: pool.env.OS,
timeouts: pool.env.Timeouts,
workdir: workdir,
sshkey: sshkey,
sshuser: sshuser,
diagnose: make(chan bool, 1),
}
if st, err := os.Stat(inst.image); err != nil && st.Size() == 0 {
// Some kernels may not need an image, however caller may still
// want to pass us a fake empty image because the rest of syzkaller
// assumes that an image is mandatory. So if the image is empty, we ignore it.
inst.image = ""
}
closeInst := inst
defer func() {
if closeInst != nil {
closeInst.Close()
}
}()
var err error
inst.rpipe, inst.wpipe, err = osutil.LongPipe()
if err != nil {
return nil, err
}
if err := inst.boot(); err != nil {
return nil, err
}
closeInst = nil
return inst, nil
}
boot() 函数主要就是各种参数判断,之后把 QEMU 起了以后 ssh 连上去,这里就不摘抄代码了:)
3. Env:单个 VM Pool 的环境变量
Env 结构体为用于一个 VM Pool 的环境变量,定义于 vm/vmimpl/vmimpl.go 中:
// Env 包含了用于 VM 池的全局常量参数.
type Env struct {
// 独特的名字
// 若几个 Pool 共享了全局命名空间则可被用于 VM name 的冲突解决
Name string
OS string // 目标 OS
Arch string // 目标 arch
Workdir string
Image string
SSHKey string
SSHUser string
Timeouts targets.Timeouts
Debug bool
Config []byte // json-序列化的 VM-类型-特定配置
KernelSrc string
}
4. Type:VM 类型
一个 VM Pool 中只能有一种类型的 VM,因而不同类型的 VM 的 Pool 应当要有不同的构造函数,在 syz-manager 中使用 Type 结构体表示一种 VM 的类型信息,定义于 vm/vmimpl/vmimpl.go 中:
type Type struct {
Ctor ctorFunc
Overcommit bool
}
type ctorFunc func(env *Env) (Pool, error)
ctorFunc 为构造函数类型,其接受一个 Env 类型的结构体指针(储存了全局的一些基本信息),并返回一个 VM Pool 实例
由一个全局的 string→Type 映射表存储了不同类型 VM 的信息,在正式启动之前程序会通过 Register() 函数将不同类型的 VM 信息注册到该表中,定义于 vm/vmimpl/vmimpl.go 中:
// Register 在包中注册一个新的 VM 类型.
func Register(typ string, ctor ctorFunc, allowsOvercommit bool) {
Types[typ] = Type{
Ctor: ctor,
Overcommit: allowsOvercommit,
}
}
//...
var(
//...
Types = make(map[string]Type)
以 QEMU 为例,其在包被导入时注册构造函数,主要是调用 LoadData() 解析配置文件后进行检查,这里不再赘叙:
func init() {
var _ vmimpl.Infoer = (*instance)(nil)
vmimpl.Register("qemu", ctor, true)
}
//...
func ctor(env *vmimpl.Env) (vmimpl.Pool, error) {
archConfig := archConfigs[env.OS+"/"+env.Arch]
cfg := &Config{
Count: 1,
CPU: 1,
Mem: 1024,
ImageDevice: "hda",
Qemu: archConfig.Qemu,
QemuArgs: archConfig.QemuArgs,
NetDev: archConfig.NetDev,
Snapshot: true,
}
if err := config.LoadData(env.Config, cfg); err != nil {
return nil, fmt.Errorf("failed to parse qemu vm config: %v", err)
}
if cfg.Count < 1 || cfg.Count > 128 {
return nil, fmt.Errorf("invalid config param count: %v, want [1, 128]", cfg.Count)
}
if env.Debug && cfg.Count > 1 {
log.Logf(0, "limiting number of VMs from %v to 1 in debug mode", cfg.Count)
cfg.Count = 1
}
if _, err := exec.LookPath(cfg.Qemu); err != nil {
return nil, err
}
if env.Image == "9p" {
if env.OS != targets.Linux {
return nil, fmt.Errorf("9p image is supported for linux only")
}
if cfg.Kernel == "" {
return nil, fmt.Errorf("9p image requires kernel")
}
} else {
if !osutil.IsExist(env.Image) {
return nil, fmt.Errorf("image file '%v' does not exist", env.Image)
}
}
if cfg.CPU <= 0 || cfg.CPU > 1024 {
return nil, fmt.Errorf("bad qemu cpu: %v, want [1-1024]", cfg.CPU)
}
if cfg.Mem < 128 || cfg.Mem > 1048576 {
return nil, fmt.Errorf("bad qemu mem: %v, want [128-1048576]", cfg.Mem)
}
cfg.Kernel = osutil.Abs(cfg.Kernel)
cfg.Initrd = osutil.Abs(cfg.Initrd)
output, err := osutil.RunCmd(time.Minute, "", cfg.Qemu, "--version")
if err != nil {
return nil, err
}
version := string(bytes.Split(output, []byte{'\n'})[0])
pool := &Pool{
env: env,
cfg: cfg,
version: version,
target: targets.Get(env.OS, env.Arch),
archConfig: archConfig,
}
return pool, nil
}
5. ResourcePool:VM 资源池队列
Guest VM 的资源调配主要是通过ResourcePool 这一结构来完成的,这实际上是一个 存放空闲 VM の idx 的单向队列,决定了 VM 的调度顺序:
type ResourcePool struct {
ids []int
mu sync.RWMutex
Freed chan interface{}
}
主要定义了这些方法来操纵资源池队列:
- Put() :向队列末尾添加空闲 VM の idx
- Len() :获取队列长度
- Take():从队列首部取出 cnt 个成员
- TakeOne() :从队列首部取出单个成员
func (pool *ResourcePool) Put(ids ...int) {
pool.mu.Lock()
defer pool.mu.Unlock()
pool.ids = append(pool.ids, ids...)
// Notify the listener.
select {
case pool.Freed <- true:
default:
}
}
func (pool *ResourcePool) Len() int {
pool.mu.RLock()
defer pool.mu.RUnlock()
return len(pool.ids)
}
//...
func (pool *ResourcePool) Take(cnt int) []int {
pool.mu.Lock()
defer pool.mu.Unlock()
totalItems := len(pool.ids)
if totalItems < cnt {
return nil
}
ret := append([]int{}, pool.ids[totalItems-cnt:]...)
pool.ids = pool.ids[:totalItems-cnt]
return ret
}
func (pool *ResourcePool) TakeOne() *int {
ret := pool.Take(1)
if ret == nil {
return nil
}
return &ret[0]
}
同时有一个 SequentialResourcePool() 函数用以初始化资源池:
func SequentialResourcePool(count int, delay time.Duration) *ResourcePool {
ret := &ResourcePool{Freed: make(chan interface{}, 1)}
go func() {
for i := 0; i < count; i++ {
ret.Put(i)
time.Sleep(delay)
}
}()
return ret
}
全局管控相关
1. Manager:基本信息
Manager 结构体用于表示一个 syz-manager 的基本信息,定义于 syz-manager/manager.go 中:
type Manager struct {
cfg *mgrconfig.Config
vmPool *vm.Pool
target *prog.Target
sysTarget *targets.Target
reporter *report.Reporter
crashdir string
serv *RPCServer
corpusDB *db.DB
startTime time.Time
firstConnect time.Time
fuzzingTime time.Duration
stats *Stats
crashTypes map[string]bool
vmStop chan bool
checkResult *rpctype.CheckArgs
fresh bool
numFuzzing uint32
numReproducing uint32
dash *dashapi.Dashboard
mu sync.Mutex
phase int
targetEnabledSyscalls map[*prog.Syscall]bool
candidates []rpctype.Candidate // untriaged inputs from corpus and hub
disabledHashes map[string]struct{}
corpus map[string]CorpusItem
seeds [][]byte
newRepros [][]byte
lastMinCorpus int
memoryLeakFrames map[string]bool
dataRaceFrames map[string]bool
saturatedCalls map[string]bool
needMoreRepros chan chan bool
hubReproQueue chan *Crash
reproRequest chan chan map[string]bool
// For checking that files that we are using are not changing under us.
// Maps file name to modification time.
usedFiles map[string]time.Time
modules []host.KernelModule
coverFilter map[uint32]uint32
coverFilterBitmap []byte
modulesInitialized bool
assetStorage *asset.Storage
}
这里只说明比较关键的几个字段:
- cfg:基本设置信息,对应存放在一个 json 文件中
- vmPool :所用的 VM Pool
- reporter:用以报告 crash
- serv :RPC Server,用以与 Guest 间通信
- corpusDB:存放语料的数据库
- targetEnabledSyscalls:测试用例所允许使用的系统调用
- candidates:待执行测试用例
- corpus:语料库
- seeds:用来对语料库变异的种子
2. fuzzing phase
syz-manager 中将 fuzzing 流程分为如下的不同阶段:
const (
// 刚刚开始,啥都没做.
phaseInit = iota
// 加载了语料库且检查了机器.
phaseLoadedCorpus
// 从语料库中分类了所有输入.
// 这是我们开始查询 hub 与最小化连续语料库的时候.
phaseTriagedCorpus
// 第一个请求发送到了 hub.
phaseQueriedHub
// 分类所有来自 hub 的新输入.
// 这是我们开始复现 crashes 的时候.
phaseTriagedHub
)
Fuzzing 结果相关
1. Crash:记录 crash 信息
manager.go 中定义了Crash 结构体用以记录产生 crash 的 VM、机器信息等,真正的 crash 信息主要存放在一个Report结构体中:
type Crash struct {
vmIndex int
hub bool // this crash was created based on a repro from hub
*report.Report
machineInfo []byte
}
2. Report:单次执行结果报告
pkg/report/rteport.go 中的 Report 结构体用以表示单次执行的结果,包括是否产生了 crash、Oops 的信息等等:
- Title:Oops 的第一行文本,用来标识特定类型的 crash
- 例如 BUG: unable to handle page fault for address: ffffffff81001619 这样的
type Report struct {
// Title contains a representative description of the first oops.
Title string
// Alternative titles, used for better deduplication.
// If two crashes have a non-empty intersection of Title/AltTitles, they are considered the same bug.
AltTitles []string
// Bug type (e.g. hang, memory leak, etc).
Type Type
// The indicative function name.
Frame string
// Report contains whole oops text.
Report []byte
// Output contains whole raw console output as passed to Reporter.Parse.
Output []byte
// StartPos/EndPos denote region of output with oops message(s).
StartPos int
EndPos int
// SkipPos is position in output where parsing for the next report should start.
SkipPos int
// Suppressed indicates whether the report should not be reported to user.
Suppressed bool
// Corrupted indicates whether the report is truncated of corrupted in some other way.
Corrupted bool
// CorruptedReason contains reason why the report is marked as corrupted.
CorruptedReason string
// Recipients is a list of RecipientInfo with Email, Display Name, and type.
Recipients vcs.Recipients
// GuiltyFile is the source file that we think is to blame for the crash (filled in by Symbolize).
GuiltyFile string
// reportPrefixLen is length of additional prefix lines that we added before actual crash report.
reportPrefixLen int
// symbolized is set if the report is symbolized.
symbolized bool
}
syz-manager 的 main() 函数其实比较简单,主要就是载入配置文件信息并调用 RunManager() :
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)
cfg, err := mgrconfig.LoadFile(*flagConfig)
if err != nil {
log.Fatalf("%v", err)
}
RunManager(cfg)
}
这一节好像没什么好说的,直接继续往下看 RunManager() 吧 :)
Step 1. 初始化 VM Pool
首先是初始化 VM Pool,这里调用了 vm/vm.go 中的 Create() 来完成 VM pool 的创建
var vmPool *vm.Pool
// "none" 类型对于调试/开发而言是一种特殊情况,manager 并不会启动任何 VM,
// 但相应的是你应当手动启动 VM 并在此启动 syz-fuzzer.
if cfg.Type != "none" {
var err error
vmPool, err = vm.Create(cfg, *flagDebug)
if err != nil {
log.Fatalf("%v", err)
}
}
该函数主要就是获取 VM 类型、封装一个 Env 结构体、调用对应类型 VM Pool 的构造函数:
// Create 创建一个可用于创建独立 VMs 的 VM pool.
func Create(cfg *mgrconfig.Config, debug bool) (*Pool, error) {
typ, ok := vmimpl.Types[cfg.Type]
if !ok {
return nil, fmt.Errorf("unknown instance type '%v'", cfg.Type)
}
env := &vmimpl.Env{
Name: cfg.Name,
OS: cfg.TargetOS,
Arch: cfg.TargetVMArch,
Workdir: cfg.Workdir,
Image: cfg.Image,
SSHKey: cfg.SSHKey,
SSHUser: cfg.SSHUser,
Timeouts: cfg.Timeouts,
Debug: debug,
Config: cfg.VM,
KernelSrc: cfg.KernelSrc,
}
impl, err := typ.Ctor(env)
if err != nil {
return nil, err
}
return &Pool{
impl: impl,
workdir: env.Workdir,
template: cfg.WorkdirTemplate,
timeouts: cfg.Timeouts,
}, nil
}
Step 2. 初始化 Manager,载入语料库,建立通信服务器
随后会创建用于存储 crash 的文件夹与一个新的 Reporter 实例:
crashdir := filepath.Join(cfg.Workdir, "crashes")
osutil.MkdirAll(crashdir)
reporter, err := report.NewReporter(cfg)
if err != nil {
log.Fatalf("%v", err)
}
接下来创建一个基本的 Manager 实例,然后是四步走:
- preloadCorpus():检查 corpus.db 文件是否存在(若不存在则创建)并载入 sys/要fuzz的OS/test 目录下的测试用模板
- 语料库载入的模板本身类似于 syzlang 文件,例如 sys/linux/pipe:
- pipe2(&(0x7f0000000000)={<r0=>0x0, <r1=>0x0}, 0x0) close(r0) close(r1)
- initStats():注册一个 prometheus 监视器(一个开源的监视&预警工具包)
- initHTTP():创建一个 HTTP 服务器并注册一系列的目录(用以供使用者访问)
- collectUsedFiles():检查所需文件是否存在
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() // 初始化 prometheus 变量.
mgr.initHTTP() // 创建 HTTP 服务.
mgr.collectUsedFiles()
之后创建一个 RPC Server,用以在 Host 与 Guest VMs 之间进行通信:
// Create 为 fuzzer 创建 PRC 服务器.
mgr.serv, err = startRPCServer(mgr)
if err != nil {
log.Fatalf("failed to create rpc server: %v", err)
}
Step 3. 初始化 dashboard 相关
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)
}
}
Step 4. 创建【日志输出】协程
接下来会新起一个协程进行数据记录的工作,内部其实就是一个每 10s 进行一次进度采集并输出日志的无限循环,主要是采集执行信息、语料覆盖率、crashes 信息等:
go func() {
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)
}
}()
Step 5. 创建 bench 协程(每隔一分钟最小化语料库并将 bench data 写入 bench 文件)
这里会判断命令行传入参数是否有 bench=,若是则调用 initBench():
if *flagBench != "" {
mgr.initBench()
}
这里的 flagBench 是一个全局的 flag 变量,golang 提供了一个 flag 包用以处理命令行参数:
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")
)
initBench() 会启动一个协程,主要就是一个每隔一分钟运行一次的循环:
- 调用 minimizeCorpus() 将语料库进行最小化
- 向 bench 参数指定的文件当中写入 语料库长度、启动时间、fuzzing 时间\n
func (mgr *Manager) initBench() {
f, err := os.OpenFile(*flagBench, os.O_WRONLY|os.O_CREATE|os.O_EXCL, osutil.DefaultFilePerm)
if err != nil {
log.Fatalf("failed to open bench file: %v", err)
}
go func() {
for {
time.Sleep(time.Minute)
vals := mgr.stats.all()
mgr.mu.Lock()
if mgr.firstConnect.IsZero() {
mgr.mu.Unlock()
continue
}
mgr.minimizeCorpus()
vals["corpus"] = uint64(len(mgr.corpus))
vals["uptime"] = uint64(time.Since(mgr.firstConnect)) / 1e9
vals["fuzzing"] = uint64(mgr.fuzzingTime) / 1e9
mgr.mu.Unlock()
data, err := json.MarshalIndent(vals, "", " ")
if err != nil {
log.Fatalf("failed to serialize bench data")
}
if _, err := f.Write(append(data, '\n')); err != nil {
log.Fatalf("failed to write bench data")
}
}
}()
}
Step 6. 启动 dashboard 协程,进入下一阶段
接下来会启动一个新的协程,主要是 每隔一分钟上报一次 syz-manager 的状态,这里不再展开 :
if mgr.dash != nil {
go mgr.dashboardReporter()
}
最后会简单检查一下 VM Pool ,随后调用 vmLoop() 进入下一阶段:
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()
}
一、VM 分组,初始化资源池等变量
一开始首先会将所有的 VM 分为两组:一组负责 fuzzing,一组负责复现 crash (maxReproVMs):
// Manager needs to be refactored (#605).
// nolint: gocyclo, gocognit, funlen
func (mgr *Manager) vmLoop() {
log.Logf(0, "booting test machines...")
log.Logf(0, "wait for the connection from test machine...")
instancesPerRepro := 4
vmCount := mgr.vmPool.Count()
maxReproVMs := vmCount - mgr.cfg.FuzzingVMs
if instancesPerRepro > maxReproVMs && maxReproVMs > 0 {
instancesPerRepro = maxReproVMs
}
随后会调用 SequentialResourcePool() 新建一个 ResourcePool 队列,主要负责对空闲 VM 使用顺序的调控 :
instances := SequentialResourcePool(vmCount, 10*time.Second*mgr.cfg.Timeouts.Scale)
接下来会初始化一系列的变量:
- runDone:保存 fuzzing 结果为 crash 的 Crash 队列
- pendingRepro:标识待复现的 Crash
- reproducing:标识某个类型 Crash 是否准备被复现
- reproQueue:Crash 的复现队列
- reproDone:Crash 的复现结果
- stopPending:等待停止标志位
- shutdown:工作终止标志位
runDone := make(chan *RunResult, 1)
pendingRepro := make(map[*Crash]bool)
reproducing := make(map[string]bool)
var reproQueue []*Crash
reproDone := make(chan *ReproResult, 1)
stopPending := false
shutdown := vm.Shutdown
最后进入到一个大循环中,这个大循环才是真正的 fuzzing 调控流程
二、外层大循环:调配空闲 VM 进行 fuzz & crash repro,等待处理不同 channel 数据
大循环的终止条件为 shutdown == nil 或是 ResourcePool 中的 VM 数量与总数量不相等,进入循环后首先会获取当前所在阶段:
for shutdown != nil || instances.Len() != vmCount {
mgr.mu.Lock()
phase := mgr.phase
mgr.mu.Unlock()
Step 1. 内层小循环:获取待复现 crash 加入复现队列
小循环会遍历 pendingRepro 中的 crash:
- 若未被复现则从 pendingRepro 中删除
- 调用 needRepro() 检查是否需要复现
- 标记该标题的 crash 已在复现,并加入复现队列中
这里的 crash.Title 其实是 Oops 的第一行文本,即同一时刻仅会复现同类 crash 中的一个:
for crash := range pendingRepro {
if reproducing[crash.Title] {
continue
}
delete(pendingRepro, crash)
if !mgr.needRepro(crash) {
continue
}
log.Logf(1, "loop: add to repro queue '%v'", crash.Title)
reproducing[crash.Title] = true
reproQueue = append(reproQueue, crash)
}
Step 2. 判断是否可以对 crash 进行复现并调控 VM
接下来会输出一行日志,之后定义一个闭包函数 canRepro,用来判断当前是否可以进行 crash 复现,主要判断以下三个条件是否满足:
- 当前阶段是否超过 phaseTriagedHub
- 待复现队列 reproQueue 是否不为空
- 加上该 crash 后所有用来复现 crash 的 VM 数量是否小于 maxReproVMs
log.Logf(1, "loop: phase=%v shutdown=%v instances=%v/%v %+v repro: pending=%v reproducing=%v queued=%v",
phase, shutdown == nil, instances.Len(), vmCount, instances.Snapshot(),
len(pendingRepro), len(reproducing), len(reproQueue))
canRepro := func() bool {
return phase >= phaseTriagedHub && len(reproQueue) != 0 &&
(int(atomic.LoadUint32(&mgr.numReproducing))+1)*instancesPerRepro <= maxReproVMs
}
接下来是两个小循环:
① 循环启动协程调度 VM 进行 crash 复现
第一个小循环会循环判断是否可以进行 crash 复现:
- 若可以复现则从资源池队列中取出一个 VM idx,若资源池为空则直接跳出
- 从 reproQueue 中取出一个 crash,更新 manager 的 numReproducing 计数
- 启动一个新的协程调用 runRepro() 对该 crash 进行复现,结果输出至 reproDone 队列中
if shutdown != nil {
for canRepro() {
vmIndexes := instances.Take(instancesPerRepro)
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)
}()
}
而 runRepro() 其实就是 repro.Run() 的 wrapper + 一些错误检查后将 VM idx 放回资源池,这里就不展开了:
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)
//...
Run() 一开始主要是一些检查,之后根据 crash 类型的不同设置不同的复现时间上限:
func Run(crashLog []byte, cfg *mgrconfig.Config, features *host.Features, reporter *report.Reporter,
vmPool *vm.Pool, vmIndexes []int) (*Result, *Stats, error) {
if len(vmIndexes) == 0 {
return nil, nil, fmt.Errorf("no VMs provided")
}
entries := cfg.Target.ParseLog(crashLog)
if len(entries) == 0 {
return nil, nil, fmt.Errorf("crash log does not contain any programs")
}
crashStart := len(crashLog)
crashTitle, crashType := "", report.Unknown
if rep := reporter.Parse(crashLog); rep != nil {
crashStart = rep.StartPos
crashTitle = rep.Title
crashType = rep.Type
}
testTimeouts := []time.Duration{
3 * cfg.Timeouts.Program, // 以捕获更简单的 crashes (即 no races and no hangs)
20 * cfg.Timeouts.Program,
cfg.Timeouts.NoOutputRunningTime, // 以捕获 "no output", races and hangs
}
switch {
case crashTitle == "":
crashTitle = "no output/lost connection"
// Lost connection 可以被更快地检测到,
// 但理论上若其由竞争造成,则可能需要最长的 timeout.
// No output 仅能在最大的 timeout 下被复现.
// 作为妥协,我们使用最小与最大的 timeouts.
testTimeouts = []time.Duration{testTimeouts[0], testTimeouts[2]}
case crashType == report.MemoryLeak:
// 由于昂贵的设置与扫描,内存泄露不能被很快地检测到.
testTimeouts = testTimeouts[1:]
case crashType == report.Hang:
testTimeouts = testTimeouts[2:]
}
接下来会将崩溃信息存储到一个 context 结构体中,并新建一个 WaitGroup:
ctx := &context{
target: cfg.SysTarget,
reporter: reporter,
crashTitle: crashTitle,
crashType: crashType,
instances: make(chan *reproInstance, len(vmIndexes)),
bootRequests: make(chan int, len(vmIndexes)),
testTimeouts: testTimeouts,
startOpts: createStartOptions(cfg, features, crashType),
stats: new(Stats),
timeouts: cfg.Timeouts,
}
ctx.reproLogf(0, "%v programs, %v VMs, timeouts %v", len(entries), len(vmIndexes), testTimeouts)
var wg sync.WaitGroup
wg.Add(len(vmIndexes))
随后循环获取用以复现的 VM idx 并依次启动新协程调用 CreateExecProgInstance() 创建 VM 并拷贝 crash 程序,若失败则休眠 10s 后重试,最多会尝试 maxTry 次;成功的结果会输出到 ctx.instances 中:
for _, vmIndex := range vmIndexes {
ctx.bootRequests <- vmIndex
go func() {
defer wg.Done()
for vmIndex := range ctx.bootRequests {
var inst *instance.ExecProgInstance
maxTry := 3
for try := 0; try < maxTry; try++ {
select {
case <-vm.Shutdown:
try = maxTry
continue
default:
}
var err error
inst, err = instance.CreateExecProgInstance(vmPool, vmIndex, cfg,
reporter, &instance.OptionalConfig{Logf: ctx.reproLogf})
if err != nil {
ctx.reproLogf(0, "failed to init instance: %v", err)
time.Sleep(10 * time.Second)
continue
}
break
}
if inst == nil {
break
}
ctx.instances <- &reproInstance{execProg: inst, index: vmIndex}
}
}()
}
// 一些收尾工作...
go func() {
wg.Wait()
close(ctx.instances)
}()
defer func() {
close(ctx.bootRequests)
for inst := range ctx.instances {
inst.execProg.VMInstance.Close()
}
}()
CreateExecProgInstance() 主要就是调用 vmPool.Create() 启动虚拟机后调用 SetupExecProg() 拷贝要执行的二进制文件,这里就不展开了:
func CreateExecProgInstance(vmPool *vm.Pool, vmIndex int, mgrCfg *mgrconfig.Config,
reporter *report.Reporter, opt *OptionalConfig) (*ExecProgInstance, error) {
vmInst, err := vmPool.Create(vmIndex)
if err != nil {
return nil, fmt.Errorf("failed to create VM: %v", err)
}
ret, err := SetupExecProg(vmInst, mgrCfg, reporter, opt)
if err != nil {
vmInst.Close()
return nil, err
}
return ret, nil
}
回到 Run() 中,其最后会调用 context.repro() 正式开始复现 crash 的工作,检查结果后返回:
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
}
repro() 函数主要分两部分:
- 调用 extractProg() 获取触发 crash 的程序集合
- func (ctx *context) repro(entries []*prog.LogEntry, crashStart int) (*Result, error) { // 去除在 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) if err != nil { return nil, err } if res == nil { return nil, nil } defer func() { if res != nil { res.Opts.Repro = false } }()
- 最小化程序集合并尝试生成可以触发该 crash 的 C 程序,返回结果:
- // 尝试最小化程序集 res, err = ctx.minimizeProg(res) if err != nil { return nil, err } // 首先尝试在不简化配置的情况下提取 C repro. res, err = ctx.extractC(res) if err != nil { return nil, err } // 简化配置并尝试提取 C repro. if !res.CRepro { res, err = ctx.simplifyProg(res) if err != nil { return nil, err } } // 简化 C 相关的配置. if res.CRepro { res, err = ctx.simplifyC(res) if err != nil { return nil, err } } return res, nil }
extractProg() 的逻辑比较简单:
- 逆序后调用 context.extractProgSingle() 逐个运行单个程序,若某一程序触发了 crash 则直接返回
- 若单一程序无法触发 crash,则调用 context.extractProgBisect() 使用二分法找出触发 crash 的程序集合
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 {
indices = append(indices, idx)
}
sort.Ints(indices)
var lastEntries []*prog.LogEntry
for i := len(indices) - 1; i >= 0; i-- {
lastEntries = append(lastEntries, entries[indices[i]])
}
for _, timeout := range ctx.testTimeouts {
// 分别执行每个程序以检测由单个程序造成的简单的 crash.
// 程序被逆序执行, 通常最后一个程序就是罪魁祸首.
res, err := ctx.extractProgSingle(lastEntries, 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
}
// 若只有一个 entry 则不进行二分.
if len(entries) == 1 {
continue
}
// 执行多个程序并二分 log 以找到造成崩溃的多个程序.
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
}
这两个函数主要就是通过如下调用链来在 VM 中执行程序,这里就不展开了:
context.testProg()
context.testProgs()
context.testWithInstance()
ExecProgInstance.RunSyzProg()
ExecProgInstance.RunSyzProgFile()
ExecProgInstance.runCommand()
② 循环启动协程进行 fuzzing
此时已经不满足可以进行 crash 复现的条件了,因而会有第二个小循环启动新协程将资源池中剩余 VM 调度去 fuzzing, 并将结果输出到 runDone 中:
for !canRepro() {
idx := instances.TakeOne()
if idx == nil {
break
}
log.Logf(1, "loop: starting instance %v", *idx)
go func() {
crash, err := mgr.runInstance(*idx)
runDone <- &RunResult{*idx, crash, err}
}()
}
}
runInstance() 函数实际上会调用 runInstanceInner(),该函数仅当产生了 Crash 时返回的结果才不为 nil,即 runRepro 队列实际上为 Crash 队列:
func (mgr *Manager) runInstance(index int) (*Crash, error) {
mgr.checkUsedFiles()
instanceName := fmt.Sprintf("vm-%d", index)
rep, vmInfo, err := mgr.runInstanceInner(index, instanceName)
machineInfo := mgr.serv.shutdownInstance(instanceName)
if len(vmInfo) != 0 {
machineInfo = append(append(vmInfo, '\n'), machineInfo...)
}
// Error that is not a VM crash.
if err != nil {
return nil, err
}
// No crash.
if rep == nil {
return nil, nil
}
crash := &Crash{
vmIndex: index,
hub: false,
Report: rep,
machineInfo: machineInfo,
}
return crash, nil
}
runInstanceInner() 的核心部分主要是:
- 调用 vmPool.Create() 创建 VM,调用 inst.Forward() 进行 TCP 转发,拷贝 syz-fuzzer 与 syz-executor 到 VM 文件系统中
- 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) if err != nil { return nil, nil, fmt.Errorf("failed to copy binary: %v", err) } // 若提供了 ExecutorBin , 这意味着 syz-executor 早已在镜像中, // 故无需进行拷贝. executorBin := mgr.sysTarget.ExecutorBin if executorBin == "" { executorBin, err = inst.Copy(mgr.cfg.ExecutorBin) 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 }
- 调用 instance.FuzzerCmd() 生成命令行后调用 inst.Run() 启动 syz-fuzzer
- // 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) 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) }
- 调用 inst.MonitorExecution() 监控 VM 运行,该函数主要是通过获取 kernel oops 来判断是否触发了 crash(KASAN 不会造成 kernel panic,从而使得一个 VM 实例长期运行,不过 dmesg 中仍有 oops)
- var vmInfo []byte rep := inst.MonitorExecution(outc, errc, mgr.reporter, vm.ExitTimeout) 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 }
Step 3. 等待处理不同 channel 数据
vmLoop() 的最后主要就是一个大的 select,等待某个 channel 中有数据后进行处理,之后重新跳回等待处理或是开始下一轮循环:
var stopRequest chan bool
if !stopPending && canRepro() {
stopRequest = mgr.vmStop
}
wait:
select {
首先是资源池的 Freed channel,在 Put() 中会将空闲 VM idx 放回资源池后向该 channel 送入一个 true,而这里什么都没有做,笔者估计会在后续版本中更新:
case <-instances.Freed:
// An instance has been released.
stopRequest 其实是 Manager.vmStop ,这个 channel 会在 VM instance 所实现的 Run() 方法中被使用:
case stopRequest <- true:
log.Logf(1, "loop: issued stop request")
stopPending = true
当 runDone 中有数据时说明fuzz 产生了 crash,此时会将产生 crash 的 VM 释放回资源池,将 crash 写入 pendingRepro 表中等待下一轮循环进行处理:
case res := <-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
}
}
reproDone 中为 crash 的复现结果,这里会保存复现结果并将对应的 crash 从 reproducing 表中删除
case res := <-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)
}
shutdown 中有数据则表示收到了终止信号,此时会将 shutdown 置为 nil,终止循环:
case <-shutdown:
log.Logf(1, "loop: shutting down...")
shutdown = nil
hubReproQueue 上也可能传来 crash,此处将其送入 pendingRepro 表中等待在后续循环中复现:
case crash := <-mgr.hubReproQueue:
log.Logf(1, "loop: get repro from hub")
pendingRepro[crash] = true
needMoreRepros 是一个传输 channel 的 channel,这里会将一个条件判断结果传入传来的 channel 中并重新跳回等待:
case reply := <-mgr.needMoreRepros:
reply <- phase >= phaseTriagedHub &&
len(reproQueue)+len(pendingRepro)+len(reproducing) == 0
goto wait
最后是 reproRequest,该 channel 意为主动进行复现的请求,这里会拷贝 reproducing 位图后将其传入传来的 channel 中:
case reply := <-mgr.reproRequest:
repros := make(map[string]bool)
for title := range reproducing {
repros[title] = true
}
reply <- repros
goto wait
}
}
}
至此,syz-manager 的基本运行逻辑分析完毕
from: https://xz.aliyun.com/t/12424
相关推荐
- mysql数据库如何快速获得库中无主键的表
-
概述总结一下MySQL数据库查看无主键表的一些sql,一起来看看吧~1、查看表主键信息--查看表主键信息SELECTt.TABLE_NAME,t.CONSTRAINT_TYPE,c.C...
- 一文读懂MySQL的架构设计
-
MySQL是一种流行的开源关系型数据库管理系统,它由四个主要组件构成:协议接入层...
- MySQL中的存储过程和函数
-
原文地址:https://dwz.cn/6Ysx1KXs作者:best.lei存储过程和函数简单的说,存储过程就是一条或者多条SQL语句的集合。可以视为批文件,但是其作用不仅仅局限于批处理。本文主要介...
- 创建数据表:MySQL 中的 CREATE 命令深入探讨
-
数据库是企业日常运营和业务发展的不可缺少的基石。MySQL是一款优秀的关系型数据库管理系统,它支持数据的插入、修改、查询和删除操作。在数据库中,表是一个关系数据库中用于保存数据的容器,它由表定义、表...
- SQL优化——IN和EXISTS谁的效率更高
-
IN和EXISTS被频繁使用在SQL中,虽然作用是一样的,但是在使用效率谁更高这点上众说纷纭。下面我们就通过一组测试来看,在不同场景下,使用哪个效率更高。...
- 在MySQL中创建新的数据库,可以使用命令,也可以通过MySQL工作台
-
摘要:在本教程中,你将学习如何使用MySQLCREATEDATABASE语句在MySQL数据库服务器上创建新数据库。MySQLCREATEDATABASE语句简介...
- SQL查找是否"存在",别再用count了
-
根据某一条件从数据库表中查询『有』与『没有』,只有两种状态,那为什么在写SQL的时候,还要SELECTCOUNT(*)呢?无论是刚入道的程序员新星,还是精湛沙场多年的程序员老白,都是一如既往...
- 解决Mysql数据库提示innodb表不存在的问题
-
发现mysql的error.log里面有报错:>InnoDB:Error:Table"mysql"."innodb_table_stats"notfo...
- Mysql实战总结&面试20问
-
1、MySQL索引使用注意事项1.1、索引哪些情况会失效查询条件包含or,可能导致索引失效如果字段类型是字符串,where时一定用引号括起来,否则索引失效...
- MySQL创建数据表
-
数据库有了后,就可以在库里面建各种数据表了。创建数据表的过程是规定数据列的属性的过程,同时也是实施数据完整性(包括实体完整性、引用完整性和域完整性)约束的过程。后面也是通过SQL语句和Navicat...
- MySQL数据库之死锁与解决方案
-
一、表的死锁产生原因:...
- MySQL创建数据库
-
我的重点还是放在数据表的操作,但第一篇还是先介绍一下数据表的容器数据库的一些操作。主要涉及数据库的创建、修改、删除和查看,下面演示一下用SQL语句创建和用图形工具创建。后面主要使用的工具是Navica...
- MySQL中创建触发器需要执行哪些操作?
-
什么是触发器触发器,就是一种特殊的存储过程。触发器和存储过程一样是一个能够完成特定功能、存储在数据库服务器上的SQL片段,但是触发器无需调用,当对数据库表中的数据执行DML操作时自动触发这个SQL片段...
- 《MySQL 入门教程》第 17 篇 MySQL 变量
-
原文地址:https://blog.csdn.net/horses/article/details/107736801原文作者:不剪发的Tony老师来源平台:CSDN变量是一个拥有名字的对象,可以用于...
- 关于如何在MySQL中创建表,看这篇文章就差不多了
-
数据库技术是现代科技领域中至关重要的一部分,而MySQL作为最流行的关系型数据库管理系统之一,在数据存储和管理方面扮演着重要角色。本文将深入探讨MySQL中CREATETABLE语句的应用,以及如何...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)