首页
社区
课程
招聘
[原创]go语言原生模糊测试:源码分析与实战
2022-4-22 19:57 14840

[原创]go语言原生模糊测试:源码分析与实战

2022-4-22 19:57
14840

前言

在2015年的GopherCon大会上 ,来自俄罗斯的google工程师Dmitry Vyukov 在其名为“[Go Dynamic Tools]”的presentation中就介绍了go-fuzz,上篇文章中,介绍了go-fuzz的安装和使用方法。go-fuzz在go的标准库中找到了200+的bug,在一些go项目的更是发现了上千的bug,可以说是go语言模糊测试的一个成功的第三方解决方案。但是,Dmitry Vyukov发现虽然通过第三方的fuzzing工具可以解决Go开发者关于Fuzzing的部分需求,但有很多功能特性是通过第三方工具无法实现的。 2016年,Dmitry Vyukov在Go官方issue列表中创建“cmd/compile: coverage instrumentation for fuzzing”的issue归纳说明了这些问题,也是从那时开始,Dmitry Vyukov极力推进Fuzzing进入Go原生工具链的。 目前go-fuzz还存在以下几个问题:

1
2
3
4
1. 可能会由于go语言内部的相互依赖的包的改变而导致崩溃
2. 不在编译器的帮助下做覆盖率的插装,这会导致极端案例代码的破坏;表现不佳; 覆盖检测质量欠佳(缺失边缘)
3. 与go语言原生的单元测试比太过复杂
4. 由于它使用源预处理,因此很难将其集成到其他构建系统和非标准上下文中

3月16日, Go 团队终于发布 Go 1.18 , Go 1.18 是一个包含大量新功能的版本,同时不仅改善了性能,也对语言本身做了有史以来最大的改变。 Go 1.18将fuzz testing纳入了go test工具链,与单元测试、性能基准测试等一起成为了Go原生测试工具链中的重要成员 ,Go也是第一个将模糊测试完全集成到其标准工具链中的主流语言 。本文从源码和实践的角度对go原生的fuzzing做一个简单的介绍。

go native fuzzing

下面是官方给出使用go test -fuzz 进行的一个模糊测试的例子,突出了它的主要组成部分。

 

显示整体模糊测试的示例代码,其中包含一个模糊目标。 在fuzz target之前是用f.Add进行语料加法,fuzz target的参数高亮显示为fuzzing参数。

 

以下是模糊测试必须遵循的规则。

  • 模糊测试必须是以 FuzzXxx 命名的函数 ,它只接受 *testing.F参数并且没有返回值。
  • 模糊测试必须在 *_test.go 文件中才能运行。
  • 模糊测试的目标必须是对(*testing.F).Fuzz的一个方法调用,且将 *testing.T作为第一个参数,然后是模糊测试的参数,没有返回值。
  • 每个模糊测试必须只有一个测试目标。
  • 所有种子语料库条目的类型必须与模糊测试参数相同,顺序相同。这适用于 (*testing.F).Add对模糊测试的 testdata/fuzz 目录中的任何语料库文件的调用。
  • 模糊测试参数只能是以下类型:
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool

go test -fuzz 相关技术

架构

gofuzz 是一个多进程的fuzzer,其组件可分为协调进程、工作进程和RPC。

 

1647775800633.png

Coordinator

​ Coordinator的职责是运行和唤醒工作进程、命令工作进行去fuzz下一个输入、如果发生crash则将interesting data 写入语料库等,该部分源码在go/src/internal/fuzz/fuzz.go 中可以找到。

  • CoordinateFuzzingOpts { }

​ 结构体 CoordinateFuzzingOpts 定义了 CoordinateFuzzing 的一系类参数,包括语料库加载后的挂钟时间、 生成和测试的随机值的数量、发现崩溃后的最小化时间、并行运行的worker进程数量、种子列表、语料库文件夹、 构成语料库条目的类型列表等,其中部分字段被设置为0值表示没有限制,其结构体源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// go/src/internal/fuzz/fuzz.go
 
// CoordinateFuzzingOpts is a set of arguments for CoordinateFuzzing.
// The zero value is valid for each field unless specified otherwise.
type CoordinateFuzzingOpts struct {
    Log io.Writer
 
    Timeout time.Duration
 
    Limit int64
 
    MinimizeTimeout time.Duration
 
    MinimizeLimit int64
 
    Parallel int
 
    Seed []CorpusEntry
 
    Types []reflect.Type
 
    CorpusDir string
 
    CacheDir string
}
  • CoordinateFuzzing()

CoordinateFuzzing函数用来创建多个worker进程,并管理worker进程对可能触发崩溃的随机输入进行测试。如果发生崩溃,该函数将返回一个err,其中包含有关崩溃的信息。

 

该函数时定义了包括主时间循环在内的诸如管理worker进程的多个行为:如 创建worker进程、开始worker进程、结束worker进程、确保发现的crash写入语料库、根据覆盖率信息协调工作进程等。

  • Coordinator {}

结构体 coordinator 定义了多个Coordinator与worker之间的channel,如coordinator传递fuzz数据到worker的channel inputC、传递最小化数据的channel minimizeC,worker传递fuzzing结果到coordinator的channel resultC等,此外还包括加载语料库后workers启动的时间 startTime 、发现的感兴趣的输入数量 interestingCount 等等,该结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// coordinator holds channels that workers can use to communicate with
// the coordinator.
type coordinator struct {
    opts CoordinateFuzzingOpts
 
    // startTime is the time we started the workers after loading the corpus.
    // Used for logging.
    startTime time.Time
 
    // inputC is sent values to fuzz by the coordinator. Any worker may receive
    // values from this channel. Workers send results to resultC.
    inputC chan fuzzInput
 
    // minimizeC is sent values to minimize by the coordinator. Any worker may
    // receive values from this channel. Workers send results to resultC.
    minimizeC chan fuzzMinimizeInput
 
    // resultC is sent results of fuzzing by workers. The coordinator
    // receives these. Multiple types of messages are allowed.
    resultC chan fuzzResult
 
    // count is the number of values fuzzed so far.
    count int64
 
    // countLastLog is the number of values fuzzed when the output was last
    // logged.
    countLastLog int64
 
    // timeLastLog is the time at which the output was last logged.
    timeLastLog time.Time
 
    // interestingCount is the number of unique interesting values which have
    // been found this execution.
    interestingCount int
 
    // warmupInputCount is the count of all entries in the corpus which will
    // need to be received from workers to run once during warmup, but not fuzz.
    // This could be for coverage data, or only for the purposes of verifying
    // that the seed corpus doesn't have any crashers. See warmupRun.
    warmupInputCount int
 
    // warmupInputLeft is the number of entries in the corpus which still need
    // to be received from workers to run once during warmup, but not fuzz.
    // See warmupInputLeft.
    warmupInputLeft int
 
    // duration is the time spent fuzzing inside workers, not counting time
    // starting up or tearing down.
    duration time.Duration
 
    // countWaiting is the number of fuzzing executions the coordinator is
    // waiting on workers to complete.
    countWaiting int64
 
    // corpus is a set of interesting values, including the seed corpus and
    // generated values that workers reported as interesting.
    corpus corpus
 
    // minimizationAllowed is true if one or more of the types of fuzz
    // function's parameters can be minimized.
    minimizationAllowed bool
 
    // inputQueue is a queue of inputs that workers should try fuzzing. This is
    // initially populated from the seed corpus and cached inputs. More inputs
    // may be added as new coverage is discovered.
    inputQueue queue
 
    // minimizeQueue is a queue of inputs that caused errors or exposed new
    // coverage. Workers should attempt to find smaller inputs that do the
    // same thing.
    minimizeQueue queue
 
    // crashMinimizing is the crash that is currently being minimized.
    crashMinimizing *fuzzResult
 
    // coverageMask aggregates coverage that was found for all inputs in the
    // corpus. Each byte represents a single basic execution block. Each set bit
    // within the byte indicates that an input has triggered that block at least
    // 1 << n times, where n is the position of the bit in the byte. For example, a
    // value of 12 indicates that separate inputs have triggered this block
    // between 4-7 times and 8-15 times.
    coverageMask []byte
}

Worker

worker的功能主要包括种子变异、最小化、运行fuzz函数、收集覆盖率、返回Crash或新的边、等。

 

worker 管理运行测试二进制文件的工作进程 ,当且仅当进程被go -test -fuzz 唤醒时,worker对象才会存在与coordinator中。 coordinator从种子语料库和缓存语料库选择输入来进行模糊测试 ,使用 workerClient 向工作进程发送 RPC , workerServer 来处理这些RPC ,下面是worker定义的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type worker struct {
    dir     string   // working directory, same as package directory
    binPath string   // path to test executable
    args    []string // arguments for test executable
    env     []string // environment for test executable
 
    coordinator *coordinator
 
    memMu chan *sharedMem // mutex guarding shared memory with worker; persists across processes.
 
    cmd         *exec.Cmd     // current worker process
    client      *workerClient // used to communicate with worker process
    waitErr     error         // last error returned by wait, set before termC is closed.
    interrupted bool          // true after stop interrupts a running worker.
    termC       chan struct{} // closed by wait when worker process terminates
}
  • workerComm

workerComm支持用于workerClint进程与workerServer进程之间通信的管道和共享内存 , 对共享内存的访问通过 RPC 协议隐式同步实现,workComm定义的结构体如下:

1
2
3
4
type workerComm struct {
    fuzzIn, fuzzOut *os.File
    memMu           chan *sharedMem // mutex guarding shared memory
}
  • workerServer

workerServer 是一个由worker进程运行的极简的 RPC 服务器,该系统允许coordinator并行运行多个worker进程,并在工作进程意外终止后从共享内存中收集导致崩溃的输入。其定义的结构体如下:

1
2
3
4
5
6
type workerServer struct {
    workerComm
    m *mutator
    coverageMask []byte
    fuzzFn func(CorpusEntry) (time.Duration, error)
}

​ 其中coverageMask定义了worker的本地覆盖数据,当新的路径被发现它会定期的更新以供coordinator参考。fuzzFn运行worker指定的fuzz目标,当发现一个crash便会返回一个error和其运行该输入花费的时间。

 

workerserver有以下几个方法:

 

server()fuzzIn 上读取序列化的 RPC 消息 , 当serve收到消息时 , 它调用相应的方法 , 然后将序列化的结果返回给fuzzout; fuzz() 在共享内存中根据随机输入在有限的持续时间或迭代次数内来运行测试函数 ,如果fuzz()发现了crash则会提前返回 ; minimizeInput() 应用一系列最小化转换, 确保每个最小化仍然会导致错误 ,或保持覆盖率;ping() 方法,coordinator调用这个方法来保证worker进程 能够调用F.Fuzz并保持通信等。

  • workerClint

workerClient 是一个极简的 RPC 客户端 ,coordinator进程使用其调用worker进程的方法。其结构体定义如下:

1
2
3
4
5
type workerClient struct {
    workerComm
    m *mutator
    mu sync.Mutex
}

其中mu为保护workerCommd管道的互斥锁。在workerClint的方法中,与workerServer大都有一个同名的方法,用来告诉worker调用指定方法,如 workClient.fuzz() 、workClient.minimize()

覆盖率&最小化

gofuzz采用覆盖率反馈的方式引导fuzzing, Dmitry在“Fuzzing support for Go”一文中曾经对coverage-guided fuzzing的引擎的工作逻辑做过如下描述:

1
2
3
4
5
6
7
start with some (potentially empty) corpus of inputs
for {
        choose a random input from the corpus
        mutate the input
        execute the mutated input and collect code coverage
        if the input gives new coverage, add it to the corpus
}

Go 编译器已经对libFuzzer提供了检测支持 ,所以在gofuzz中重用了该部分。 编译器为每个基本块添加一个 8 位计数器用来统计覆盖率。 当coordinator接收到产生新覆盖范围的输入时,它会将该worker进程的覆盖范围与当前组合的覆盖范围数组进行比较:如果另一个worker进程已经发现了提供相同覆盖范围的输入,则把该输入丢弃。如果新的输入确实提供了新的覆盖,则coordinator将其发送回worker进程(可能是不同的worker)以进行最小化处理。输入越小,执行的速度往往就越快, coordinator会将最小化的输入添加到缓存语料库中,之后发送给worker以进行进一步的模糊测试 。

 

coordinator收到导致错误的输入时,它会再次将输入发送回worker进程以进行最小化。在这种情况下,工作人员尝试找到仍然会导致错误的较小输入,尽管不一定是相同的错误。输入最小化后,coordinator将其保存到testdata/corpus/$FuzzTarget,然后关闭工作进程,以非零状态退出。

 

gofuzz实现输入最小化主要通过四个循环:

  1. 尝试通过2分法剪去尾巴字节
  2. 尝试删除每个单独的字节
  3. 尝试删除每个可能的字节子集
  4. 尝试替换每个字节为可打印的简单可读字节

最小化的相关代码在go/src/internal/fuzz/minimize.go 中.

变异

go/src/internal/fuzz/mutator.go 实现了对初始文件的变异功能,其核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func (m *mutator) mutate(vals []any, maxBytes int) {
    maxPerVal := maxBytes/len(vals) - 100
    i := m.rand(len(vals))
    switch v := vals[i].(type) {
    case int:
        vals[i] = int(m.mutateInt(int64(v), maxInt))
    case int8:
        vals[i] = int8(m.mutateInt(int64(v), math.MaxInt8))
    case int16:
        vals[i] = int16(m.mutateInt(int64(v), math.MaxInt16))
    case int64:
        vals[i] = m.mutateInt(v, maxInt)
    case uint:
        vals[i] = uint(m.mutateUInt(uint64(v), maxUint))
    case uint16:
        vals[i] = uint16(m.mutateUInt(uint64(v), math.MaxUint16))
    case uint32:
        vals[i] = uint32(m.mutateUInt(uint64(v), math.MaxUint32))
    case uint64:
        vals[i] = m.mutateUInt(uint64(v), maxUint)
    case float32:
        vals[i] = float32(m.mutateFloat(float64(v), math.MaxFloat32))
    case float64:
        vals[i] = m.mutateFloat(v, math.MaxFloat64)
    case bool:
        if m.rand(2) == 1 {
            vals[i] = !v // 50% chance of flipping the bool
        }
    case rune: // int32
        vals[i] = rune(m.mutateInt(int64(v), math.MaxInt32))
    case byte: // uint8
        vals[i] = byte(m.mutateUInt(uint64(v), math.MaxUint8))
    case string:
        ...
    case []byte:
        ...
    default:
        panic(fmt.Sprintf("type not supported for mutating: %T", vals[i]))
    }
}

gofuzz 目前支持的类型有:string, []byteint, int8, int16, int32/rune, int64uint, uint8/byte, uint16, uint32, uint64float32, float64bool。以int型为例,可以看到go-native-fuzz对该型的变异方式还比较单一,加上或减去一个随机数,并判断其变异后的返回值不能超高int支持的最大范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (m *mutator) mutateInt(v, maxValue int64) int64 {
    var max int64
    for {
        max = 100
        switch m.rand(2) {
        case 0:
            // Add a random number
            if v >= maxValue {
                continue
            }
            if v > 0 && maxValue-v < max {
                // Don't let v exceed maxValue
                max = maxValue - v
            }
            v += int64(1 + m.rand(int(max)))
            return v
        case 1:
            // Subtract a random number
            if v <= -maxValue {
                continue
            }
            if v < 0 && maxValue+v < max {
                // Don't let v drop below -maxValue
                max = maxValue + v
            }
            v -= int64(1 + m.rand(int(max)))
            return v
        }
    }
}

而对于byte[]型的值变异会比较多样化,比如增、删、改、插、交换、亦或等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var byteSliceMutators = []byteSliceMutator{
    byteSliceRemoveBytes,
    byteSliceInsertRandomBytes,
    byteSliceDuplicateBytes,
    byteSliceOverwriteBytes,
    byteSliceBitFlip,
    byteSliceXORByte,
    byteSliceSwapByte,
    byteSliceArithmeticUint8,
    byteSliceArithmeticUint16,
    byteSliceArithmeticUint32,
    byteSliceArithmeticUint64,
    byteSliceOverwriteInterestingUint8,
    byteSliceOverwriteInterestingUint16,
    byteSliceOverwriteInterestingUint32,
    byteSliceInsertConstantBytes,
    byteSliceOverwriteConstantBytes,
    byteSliceShuffleBytes,
    byteSliceSwapBytes,
}
 
func (m *mutator) mutateBytes(ptrB *[]byte)
 
func (m *mutator) mutateFloat(v, maxValue float64) float64
 
func (m *mutator) mutateUInt(v, maxValue uint64) uint64
 
func (m *mutator) mutateInt(v, maxValue int64) int64
...

项目实战 :yaml

项目地址:https://github.com/go-yaml/yaml

 

该项目是作为 juju 项目的一部分在 Canonical 中开发的,基于著名的 libyaml C 库的纯 Go 端口,可以快速可靠地解析和生成 YAML 数据 ,使 Go 程序能够轻松地编码和解码 YAML 值 。

 

yaml.Unmarshal()函数解码在字节切片中找到的第一个文档,并将解码后的值赋给输出值,十分适合作为我们测试的对象。

 

首先将项目下载的本地,然后git checkout 切换到分支 v3,之后编写fuzzing函数,创建文件fuzz_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
package yaml_test
 
import (
    "testing"
    "gopkg.in/yaml.v3"
)
func FuzzUnmarshal(f *testing.F){
    f.Add( []byte{1})
    f.Fuzz(func(t *testing.T,num []byte){
        var v interface{}
        _ = yaml.Unmarshal([]byte(num),&v)
    })
}

go test -fuzz =Fuzz 开始fuzz

1
2
3
4
5
6
7
8
9
10
11
12
13
null@ubuntu:~/gowork/src/github.com/yaml$ go test -fuzz=Fuzz
OK: 45 passed
fuzz: elapsed: 0s, gathering baseline coverage: 0/1 completed
fuzz: elapsed: 0s, gathering baseline coverage: 1/1 completed, now fuzzing with 2 workers
fuzz: elapsed: 3s, execs: 61041 (20341/sec), new interesting: 129 (total: 130)
fuzz: elapsed: 6s, execs: 142873 (27284/sec), new interesting: 199 (total: 200)
fuzz: elapsed: 9s, execs: 212708 (23280/sec), new interesting: 239 (total: 240)
fuzz: elapsed: 12s, execs: 274044 (20439/sec), new interesting: 271 (total: 272)
fuzz: elapsed: 15s, execs: 320924 (15631/sec), new interesting: 298 (total: 299)
fuzz: elapsed: 18s, execs: 389403 (22820/sec), new interesting: 317 (total: 318)
fuzz: elapsed: 21s, execs: 423864 (11490/sec), new interesting: 334 (total: 335)
fuzz: elapsed: 24s, execs: 444293 (6809/sec), new interesting: 344 (total: 345)
......

可选参数:

  • -fuzztime: fuzz 目标在退出前将执行的总时间或迭代次数,默认为无限期。
  • -fuzzminimizetime:在每次最小化尝试期间执行模糊目标的时间或迭代次数,默认为 60 秒。
  • -parallel:一次运行的模糊测试进程的数量,默认值 $GOMAXPROCS。目前,在 fuzzing 期间设置 -cpu 无效。

    第一行表示在开始模糊测试之前收集了“基线覆盖率”。

  • elapsed:自进程开始以来经过的时间量

  • execs:针对模糊目标运行的输入总数(自最后一个日志行以来的平均 execs/sec)
  • new interesting:已添加到生成的语料库中的“有趣”输入的总数(与整个语料库的总大小)

出现panic之后立马返回,停止fuzz。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
--- FAIL: FuzzUnmarshal (362.91s)
    --- FAIL: FuzzUnmarshal (0.00s)
        testing.go:1349: panic: internal error: attempted to parse unknown event (please report): none
            goroutine 1422926 [running]:
            runtime/debug.Stack()
                /usr/local/go/src/runtime/debug/stack.go:24 +0x90
            testing.tRunner.func1()
                /usr/local/go/src/testing/testing.go:1349 +0x1f2
            panic({0x6be7a0, 0xc00d77d900})
                /usr/local/go/src/runtime/panic.go:838 +0x207
            gopkg.in/yaml%2ev3.handleErr(0xc0013a36e0)
                /home/null/gowork/src/github.com/yaml/yaml.go:294 +0xc5
            panic({0x6be7a0, 0xc00d77d900})
                /usr/local/go/src/runtime/panic.go:838 +0x207
            gopkg.in/yaml%2ev3.(*parser).parse(0xc00d80b800)
                /home/null/gowork/src/github.com/yaml/decode.go:163 +0x2d9
            gopkg.in/yaml%2ev3.unmarshal({0xc00d653b50, 0x5, 0x10}, {0x6b5ce0?, 0xc00d77d8f0}, 0x0?)
                /home/null/gowork/src/github.com/yaml/yaml.go:161 +0x373
            gopkg.in/yaml%2ev3.Unmarshal(...)
                /home/null/gowork/src/github.com/yaml/yaml.go:89
            gopkg.in/yaml%2ev3_test.FuzzUnmarshal.func1(0x0?, {0xc00d653b50, 0x5, 0x10})
                /home/null/gowork/src/github.com/yaml/fuzz_test.go:11 +0x55
            reflect.Value.call({0x6c14a0?, 0x718e40?, 0x13?}, {0x7041e6, 0x4}, {0xc00d8086c0, 0x2, 0x2?})
                /usr/local/go/src/reflect/value.go:556 +0x845
            reflect.Value.Call({0x6c14a0?, 0x718e40?, 0x514?}, {0xc00d8086c0, 0x2, 0x2})
                /usr/local/go/src/reflect/value.go:339 +0xbf
            testing.(*F).Fuzz.func1.1(0x0?)
                /usr/local/go/src/testing/fuzz.go:337 +0x231
            testing.tRunner(0xc00d7ff040, 0xc00d80e000)
                /usr/local/go/src/testing/testing.go:1439 +0x102
            created by testing.(*F).Fuzz.func1
                /usr/local/go/src/testing/fuzz.go:324 +0x5b8
 
 
    Failing input written to testdata/fuzz/FuzzUnmarshal/b27ab1d6a899fb4f0607968de5b80e930b49a0e279bd4341c515ebf9bd7e7c78
    To re-run:
    go test -run=FuzzUnmarshal/b27ab1d6a899fb4f0607968de5b80e930b49a0e279bd4341c515ebf9bd7e7c78
FAIL
exit status 1

出现崩溃后, 模糊引擎会将导致崩溃的输入写入该测试的种子语料库中,而且此输入会当作执行go test命令时的默认输入。根据奔溃提示,执行 go test run=FuzzUnmarshal/b27ab1d6a899fb4f0607968de5b80e930b49a0e279bd4341c515ebf9bd7e7c78 复现。

1
2
3
4
5
6
null@ubuntu:~/gowork/src/github.com/yaml$ go test -run=FuzzUnmarshal/b27ab1d6a899fb4f0607968de5b80e930b49a0e279bd4341c515ebf9bd7e7c78
--- FAIL: FuzzUnmarshal (0.00s)
    --- FAIL: FuzzUnmarshal/b27ab1d6a899fb4f0607968de5b80e930b49a0e279bd4341c515ebf9bd7e7c78 (0.00s)
panic: internal error: attempted to parse unknown event (please report): none [recovered]
    panic: internal error: attempted to parse unknown event (please report): none [recovered]
    panic: internal error: attempted to parse unknown event (please report): none

查看崩溃字符

1
2
3
null@ubuntu:~/gowork/src/github.com/yaml$ cat testdata/fuzz/FuzzUnmarshal/b27ab1d6a899fb4f0607968de5b80e930b49a0e279bd4341c515ebf9bd7e7c78
go test fuzz v1
[]byte(":   \xf0")

其中第一行显示了模糊引擎文件的编码版本,下面的是构成语料库条目的值,即导致程序崩溃的输入。

 

再次运行go test单元测试时,会将该输入当作默认输入并引发崩溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
null@ubuntu:~/gowork/src/github.com/yaml$ go test
OK: 45 passed
--- FAIL: FuzzUnmarshal (0.00s)
    --- FAIL: FuzzUnmarshal/b27ab1d6a899fb4f0607968de5b80e930b49a0e279bd4341c515ebf9bd7e7c78 (0.00s)
panic: internal error: attempted to parse unknown event (please report): none [recovered]
    panic: internal error: attempted to parse unknown event (please report): none [recovered]
    panic: internal error: attempted to parse unknown event (please report): none
 
goroutine 70 [running]:
testing.tRunner.func1.2({0x6297a0, 0xc001470350})
    /usr/local/go/src/testing/testing.go:1389 +0x24e
testing.tRunner.func1()
    /usr/local/go/src/testing/testing.go:1392 +0x39f
panic({0x6297a0, 0xc001470350})
    /usr/local/go/src/runtime/panic.go:838 +0x207
gopkg.in/yaml%2ev3.handleErr(0xc0000d16e0)
    /home/null/gowork/src/github.com/yaml/yaml.go:294 +0x6d
panic({0x6297a0, 0xc001470350})
    /usr/local/go/src/runtime/panic.go:838 +0x207
gopkg.in/yaml%2ev3.(*parser).parse(0xc0017f7400)
    /home/null/gowork/src/github.com/yaml/decode.go:163 +0x194
gopkg.in/yaml%2ev3.unmarshal({0xc0017de7e8, 0x5, 0x8}, {0x620ce0?, 0xc001470330}, 0x0?)
    /home/null/gowork/src/github.com/yaml/yaml.go:161 +0x306
gopkg.in/yaml%2ev3.Unmarshal(...)
    /home/null/gowork/src/github.com/yaml/yaml.go:89
gopkg.in/yaml%2ev3_test.FuzzUnmarshal.func1(0x0?, {0xc0017de7e8, 0x5, 0x8})
    /home/null/gowork/src/github.com/yaml/fuzz_test.go:11 +0x55
reflect.Value.call({0x62c4a0?, 0x683e48?, 0x13?}, {0x66f1e6, 0x4}, {0xc0017fc570, 0x2, 0x2?})
    /usr/local/go/src/reflect/value.go:556 +0x845
reflect.Value.Call({0x62c4a0?, 0x683e48?, 0x514?}, {0xc0017fc570, 0x2, 0x2})
    /usr/local/go/src/reflect/value.go:339 +0xbf
testing.(*F).Fuzz.func1.1(0xc00003d760?)
    /usr/local/go/src/testing/fuzz.go:337 +0x231
testing.tRunner(0xc000084ea0, 0xc0017e8510)
    /usr/local/go/src/testing/testing.go:1439 +0x102
created by testing.(*F).Fuzz.func1
    /usr/local/go/src/testing/fuzz.go:324 +0x5b8
exit status 2
FAIL    gopkg.in/yaml.v3    3.033s

使用": \xf0"验证该输入是否能正常触发panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
 
import(
    "fmt"
    "gopkg.in/yaml.v3"
)
 
func main(){
    in := ":   \xf0"
    var n yaml.Node
    if err := yaml.Unmarshal([]byte(in),&n);err != nil {
        fmt.Println(err)
    }
}

运行,成功触发,证明确实存在问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
null@ubuntu:~/gowork/src/github.com/yaml/fuzztest$ go run fuzz_check.go
panic: internal error: attempted to parse unknown event (please report): none [recovered]
    panic: internal error: attempted to parse unknown event (please report): none
 
goroutine 1 [running]:
gopkg.in/yaml%2ev3.handleErr(0xc000047ed8)
    /home/null/gowork/src/github.com/yaml/yaml.go:294 +0x6d
panic({0x4e60e0, 0xc000010380})
    /usr/local/go/src/runtime/panic.go:838 +0x207
gopkg.in/yaml%2ev3.(*parser).parse(0xc00003ac00)
    /home/null/gowork/src/github.com/yaml/decode.go:163 +0x194
gopkg.in/yaml%2ev3.unmarshal({0xc00001a648, 0x5, 0x8}, {0x4f21e0?, 0xc000104320}, 0x0?)
    /home/null/gowork/src/github.com/yaml/yaml.go:161 +0x306
gopkg.in/yaml%2ev3.Unmarshal(...)
    /home/null/gowork/src/github.com/yaml/yaml.go:89
main.main()
    /home/null/gowork/src/github.com/yaml/fuzztest/fuzz_check.go:11 +0x51
exit status 2

查看原因,根据堆栈跟踪,在源码github.com/yaml/yaml.go:161github.com/yaml/decode.go:163在加上两句print语句,查看导致panic的值。

 

1647441645602.png

 

1647441921683.png

 

运行结果如下,程序已经无法正常解析该输入,具体原因还未搞清,该问题在github上已经有人提出,不过目前仍然未被解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
null@ubuntu:~/gowork/src/github.com/yaml/fuzztest$ go run fuzz_check.go
p: &{{0  0 0 {0 0 0}  {0 0 0} 0x4b8a00 <nil> [58 32 32 32 240] 0 false [] 0 0 0 [] 0 0 0 {0 0 0} [] [] [] [] [] [] 0 false false 0 [] 0 0 false 0 [] false [] map[] 0 [] [] [] [] <nil>} {0 {0 0 0} {0 0 0} 0 <nil> [] [] [] [] [] [] [] [] false false 0} <nil> map[] false false}
p.peek: none
panic: internal error: attempted to parse unknown event (please report): none [recovered]
    panic: internal error: attempted to parse unknown event (please report): none
 
goroutine 1 [running]:
gopkg.in/yaml%2ev3.handleErr(0xc000047ec8)
    /home/null/gowork/src/github.com/yaml/yaml.go:295 +0x6d
panic({0x4e70e0, 0xc000010380})
    /usr/local/go/src/runtime/panic.go:838 +0x207
gopkg.in/yaml%2ev3.(*parser).parse(0xc00003ac00)
    /home/null/gowork/src/github.com/yaml/decode.go:164 +0x1f4
gopkg.in/yaml%2ev3.unmarshal({0xc00001a648, 0x5, 0x8}, {0x4f31e0?, 0xc00007e320}, 0x0?)
    /home/null/gowork/src/github.com/yaml/yaml.go:162 +0x358
gopkg.in/yaml%2ev3.Unmarshal(...)
    /home/null/gowork/src/github.com/yaml/yaml.go:89
main.main()
    /home/null/gowork/src/github.com/yaml/fuzztest/fuzz_check.go:11 +0x51
exit status 2

更新:目前CVE-2022-28948表示该漏洞
8a52aa8afc2fafe88779b532522c683.png

Limitations

  1. 仅仅支持[]byte 和原始类型,不支持struct、slice和array
  2. 在同一个pkg里不同运行多个fuzzer
  3. 遇到crash会立即停止fuzz
  4. 不能直接将现存的文件转换到语料库的格式

在 github上也可以在带有标签的issue上找到一些未解决和待改进的地方

 

https://github.com/golang/go/issues?q=is%3Aissue+is%3Aopen+label%3Afuzz

参考链接

https://speakerdeck.com/david74chou/fuzzying-test-in-go?slide=20

 

https://go.dev/doc/fuzz/

 

https://pkg.go.dev/testing@master#F

 

https://github.com/golang/go/issues/14565

 

https://tonybai.com/2021/12/01/first-class-fuzzing-in-go-1-18/

 

https://jayconrod.com/posts/123/internals-of-go-s-new-fuzzing-system


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2022-5-26 17:02 被ZxyNull编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回