fuzz 包

介绍

提供仓颉 fuzz 引擎及对应的接口,需配合覆盖率反馈插桩(SanitizerCoverage)功能使用,编写代码对 API 进行测试。使用此包需要开发者对 fuzz 测试有一定的了解,初学者建议先学习 C 语言的 Fuzz 工具 libFuzzer

使用本包需要外部依赖 LLVM 套件 compiler-rt 提供的 libclang_rt.fuzzer_no_main.a 静态库,当前支持 Linux 以及 macOS,不支持 Windows,对 cjnative 后端支持最好,对 cjvm 后端的测试需要重新链接依赖库。

通常使用包管理工具即可完成安装,例如 Ubuntu 22.04 系统上可使用 sudo apt install clang 进行安装,安装后可以在 clang -print-runtime-dir 指向的目录下找到对应的 libclang_rt.fuzzer_no_main.a 文件,例如 /usr/lib/llvm-14/lib/clang/14.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.a,将来在链接时会用到它。

主要接口

FUZZ_VERSION

public let FUZZ_VERSION = "1.0.0"

功能:Fuzz 版本。

class Fuzzer

public class Fuzzer {
    public init(targetFunction: (Array<UInt8>) -> Int32)
    public init(targetFunction: (Array<UInt8>) -> Int32, args: Array<String>)
    public init(targetFunction: (DataProvider) -> Int32)
    public init(targetFunction: (DataProvider) -> Int32, args: Array<String>)
}

Fuzzer 类提供了 fuzz 工具的创建.

init

public init(targetFunction: (Array<UInt8>) -> Int32)

功能:根据以 UInt8 数组为参数,以 Int32 为返回值的目标函数,创建 Fuzzer 实例。

参数:

  • targetFunction:以 UInt8 数组为参数,以 Int32 为返回值的目标函数

init

public init(targetFunction: (Array<UInt8>) -> Int32, args: Array<String>)

功能:根据以 UInt8 数组为参数,以 Int32 为返回值的目标函数,以及 Fuzz 运行参数,创建 Fuzzer 实例。

参数:

  • targetFunction:以 UInt8 数组为参数,以 Int32 为返回值的目标函数
  • args:Fuzz 运行参数

init

public init(targetFunction: (DataProvider) -> Int32)

功能:根据以 DataProvider 为参数,以 Int32 为返回值的目标函数,创建 Fuzzer 实例。

参数:

  • targetFunction:以 DataProvider 为参数,以 Int32 为返回值的目标函数

init

public init(targetFunction: (DataProvider) -> Int32, args: Array<String>)

功能:根据以 DataProvider 为参数,以 Int32 为返回值的目标函数,以及 Fuzz 运行参数,创建 Fuzzer 实例。

参数:

  • targetFunction:以 DataProvider 为参数,以 Int32 为返回值的目标函数
  • args:Fuzz 运行参数

func getArgs

public func getArgs(): Array<String>

功能:获取 Fuzz 运行参数。

返回值:当前 Fuzz 运行参数

func setArgs

public func setArgs(args: Array<String>): Unit

功能:设置 Fuzz 运行参数。

参数:

  • args:Fuzz 运行参数。

func setTargetFunction

public func setTargetFunction(targetFunction: (Array<UInt8>) -> Int32): Unit

功能:设置 Fuzz 目标函数。

参数:

  • targetFunction:以 UInt8 数组为参数,以 Int32 为返回值的目标函数

func setTargetFunction

public func setTargetFunction(targetFunction: (DataProvider) -> Int32): Unit

功能:设置 Fuzz 目标函数。

参数:

  • targetFunction:以 DataProvider 为参数,以 Int32 为返回值的目标函数

func startFuzz

public func startFuzz(): Unit

功能:执行 Fuzz。

func enableFakeCoverage

public func enableFakeCoverage(): Unit

功能:创建一块虚假的覆盖率反馈区域,保持 Fuzz 持续进行。在 DataProvider 模式下,前几轮运行时可能由于数据不足而导致没有覆盖率,libfuzzer 会退出。该功能默认为关闭。

func disableFakeCoverage

public func disableFakeCoverage(): Unit

功能:关闭调用 enableFakeCoverage 对 Fuzz 的影响。

func enableDebugDataProvider

public func enableDebugDataProvider(): Unit

功能:启用调试信息打印功能,当 DataProvider.consumeXXX 被调用时,返回值将被打印到 stdout。该功能默认为关闭。

func disableDebugDataProvider

public func disableDebugDataProvider(): Unit

功能:关闭调试信息打印功能,当 DataProvider.consumeXXX 被调用时,返回值将不被打印到 stdout

class FuzzerBuilder

public class FuzzerBuilder {
    public init(targetFunction: (Array<UInt8>) -> Int32)
    public init(targetFunction: (DataProvider) -> Int32)
}

此类用于 Fuzzer 类的构建。

init

public init(targetFunction: (Array<UInt8>) -> Int32)

功能:根据以 UInt8 数组为参数,以 Int32 为返回值的目标函数,创建 FuzzerBuilder 实例。

参数:

  • targetFunction:以 UInt8 数组为参数,以 Int32 为返回值的目标函数

init

public init(targetFunction: (DataProvider) -> Int32)

功能:根据以 DataProvider 为参数,以 Int32 为返回值的目标函数,创建 FuzzerBuilder 实例。

参数:

  • targetFunction:以 DataProvider 为参数,以 Int32 为返回值的目标函数

func setArgs

public func setArgs(args: Array<String>): FuzzerBuilder

功能:设置 Fuzz 运行参数。

参数:

  • args:Fuzz 运行参数

返回值:当前 FuzzerBuilder 实例。

func setTargetFunction

public func setTargetFunction(targetFunction: (Array<UInt8>) -> Int32): FuzzerBuilder

功能:设置 Fuzz 目标函数。

参数:

  • targetFunction:以 UInt8 数组为参数,以 Int32 为返回值的目标函数

返回值:当前 FuzzerBuilder 实例。

func setTargetFunction

public func setTargetFunction(targetFunction: (DataProvider) -> Int32): FuzzerBuilder

功能:设置 Fuzz 目标函数。

参数:

  • targetFunction:以 DataProvider 为参数,以 Int32 为返回值的目标函数

返回值:当前 FuzzerBuilder 实例。

func build

public func build(): Fuzzer

功能:生成一个 Fuzzer 实例。

返回值:Fuzzer 实例

class DataProvider

public open class DataProvider {
    public let data: Array<UInt8>
    public var remainingBytes: Int64
    public var offset: Int64
}

DataProvider 是一个工具类,目的是将变异数据的字节流转化为标准的仓颉基本数据,当前支持的数据结构如下:

目标类型API说明
BoolconsumeBool()获取1个Bool,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeBools(count: Int64)获取N个Bool,变异数据长度不足时,抛出ExhaustedException
ByteconsumeByte()获取1个Byte,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeBytes(count: Int64)获取N个Byte,变异数据长度不足时,抛出ExhaustedException
UInt8consumeUInt8()获取1个UInt8,变异数据长度不足时,抛出ExhaustedException
UInt16consumeUInt16()获取1个UInt16,变异数据长度不足时,抛出ExhaustedException
UInt32consumeUInt32()获取1个UInt32,变异数据长度不足时,抛出ExhaustedException
UInt64consumeUInt64()获取1个UInt64,变异数据长度不足时,抛出ExhaustedException
Int8consumeInt8()获取1个Int8,变异数据长度不足时,抛出ExhaustedException
Int16consumeInt16()获取1个Int16,变异数据长度不足时,抛出ExhaustedException
Int32consumeInt32()获取1个Int32,变异数据长度不足时,抛出ExhaustedException
Int32consumeInt32()获取1个Int32,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeUInt8s(count: Int64)获取N个UInt8,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeUInt16s(count: Int64)获取N个UInt16,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeUInt32s(count: Int64)获取N个UInt32,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeUInt64s(count: Int64)获取N个UInt64,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeInt8s(count: Int64)获取N个Int8,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeInt16s(count: Int64)获取N个Int16,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeInt32s(count: Int64)获取N个Int32,变异数据长度不足时,抛出ExhaustedException
ArrayconsumeInt32s(count: Int64)获取N个Int32,变异数据长度不足时,抛出ExhaustedException
CharconsumeChar()获取1个Char,变异数据长度不足时,抛出ExhaustedException
StringconsumeAsciiString(maxLength: Int64)获取1个纯ASCII的String,长度为0到maxLength,可以为0
StringconsumeString(maxLength: Int64)获取1个UTF8 String,长度为0到maxLength,可以为0
ArrayconsumeAll()将DataProvider中的剩余内容全部转化为字节数组
StringconsumeAllAsAscii()将DataProvider中的剩余内容全部转化为纯ASCII的String
StringconsumeAllAsString()将DataProvider中的剩余内容全部转化为UTF8 String,末尾多余的字符不会被消耗

不支持Float32和Float64

在数据长度不足时,调用上述大部分虽然会抛出 ExhaustedException,但编写 fuzz 函数时通常并不需要对它进行处理,ExhaustedException 默认会被 fuzz 框架捕获,告诉 libfuzzer 该次运行无效,请继续下一轮变异。随着执行时间的变长,变异数据也会逐渐变长,直到满足 DataProvider 的需求。

如果达到了 max_len 仍无法满足 DataProvider 的需求,则进程退出,请修改 fuzz 测试用例(推荐) 或 增大 max_len(不推荐)。

data

public let data: Array<UInt8>

功能:变异数据。

remainingBytes

public var remainingBytes: Int64

功能:剩余字节数。

offset

public var offset: Int64

功能:已转化的字节数。

func withCangjieData

public static func withCangjieData(data: Array<UInt8>): DataProvider

功能:根据 UInt8 数组创建 DataProvider 实例。

参数:

  • data:字节流的变异数据

func withNativeData

public static unsafe func withNativeData(data: CPointer<UInt8>, length: Int64): DataProvider

功能:根据 UInt8 指针,以及数据长度,创建 DataProvider 实例。

参数:

  • data:利用指针存储的变异数据
  • length:数据长度

func consumeBool

public open func consumeBool(): Bool

功能:将数据转换成 Bool 类型实例。

返回值:Bool 类型实例

func consumeBools

public open func consumeBools(count: Int64): Array<Bool>

功能:将指定数量的数据转换成 Bool 类型数组。

参数:

  • count:指定转换的数据量

返回值:Bool 类型数组

func consumeByte

public open func consumeByte(): Byte

功能:将数据转换成 Byte 类型实例。

返回值:Byte 类型实例

func consumeBytes

public open func consumeBytes(count: Int64): Array<Byte>

功能:将指定数量的数据转换成 Byte 类型数组。

参数:

  • count:指定转换的数据量

返回值:Byte 类型数组

func consumeUInt8

public open func consumeUInt8(): UInt8

功能:将数据转换成 UInt8 类型实例。

返回值:UInt8 类型实例

func consumeUInt16

public open func consumeUInt16(): UInt16

功能:将数据转换成 UInt16 类型实例。

返回值:UInt16 类型实例

func consumeUInt32

public open func consumeUInt32(): UInt32

功能:将数据转换成 UInt32 类型实例。

返回值:UInt32 类型实例

func consumeUInt64

public open func consumeUInt64(): UInt64

功能:将数据转换成 UInt64 类型实例。

返回值:UInt64 类型实例

func consumeInt8

public open func consumeInt8(): Int8

功能:将数据转换成 Int8 类型实例。

返回值:Int8 类型实例

func consumeInt16

public open func consumeInt16(): Int16

功能:将数据转换成 Int16 类型实例。

返回值:Int16 类型实例

func consumeInt32

public open func consumeInt32(): Int32

功能:将数据转换成 Int32 类型实例。

返回值:Int32 类型实例

func consumeInt64

public open func consumeInt64(): Int64

功能:将数据转换成 Int64 类型实例。

返回值:Int64 类型实例

func consumeUInt8s

public open func consumeUInt8s(count: Int64): Array<UInt8>

功能:将指定数量的数据转换成 UInt8 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt8 类型数组

func consumeUInt16s

public open func consumeUInt16s(count: Int64): Array<UInt16>

功能:将指定数量的数据转换成 UInt16 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt16 类型数组

func consumeUInt32s

public open func consumeUInt32s(count: Int64): Array<UInt32>

功能:将指定数量的数据转换成 UInt32 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt32 类型数组

func consumeUInt64s

public open func consumeUInt64s(count: Int64): Array<UInt64>

功能:将指定数量的数据转换成 UInt64 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt64 类型数组

func consumeInt8s

public open func consumeInt8s(count: Int64): Array<Int8>

功能:将指定数量的数据转换成 Int8 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int8 类型数组

func consumeInt16s

public open func consumeInt16s(count: Int64): Array<Int16>

功能:将指定数量的数据转换成 Int16 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int16 类型数组

func consumeInt32s

public open func consumeInt32s(count: Int64): Array<Int32>

功能:将指定数量的数据转换成 Int32 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int32 类型数组

func consumeInt64s

public open func consumeInt64s(count: Int64): Array<Int64>

功能:将指定数量的数据转换成 Int64 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int64 类型数组

func consumeFloat32

public open func consumeFloat32(): Float32

功能:将数据转换成 Float32 类型实例(保留接口,功能暂未实现,当前为抛出异常UnsupportException)。

返回值:Float32 类型实例

func consumeFloat64

public open func consumeFloat64(): Float64

功能:将数据转换成 Float64 类型实例(保留接口,功能暂未实现,当前为抛出异常UnsupportException)。

返回值:Float64 类型实例

func consumeChar

public open func consumeChar(): Char

功能:将数据转换成 Char 类型实例。

返回值:Char 类型实例

func consumeAsciiString

public open func consumeAsciiString(maxLength: Int64): String

功能:将数据转换成 Ascii String 类型实例。

参数:

  • maxLengthString 类型的最大长度

返回值:String 类型实例

func consumeString

public open func consumeString(maxLength: Int64): String

功能:将数据转换成 utf8 String 类型实例。

参数:

  • maxLengthString 类型的最大长度

返回值:String 类型实例

func consumeAll

public open func consumeAll(): Array<UInt8>

功能:将所有数据转换成 UInt8 类型数组。

返回值:UInt8 类型数组

func consumeAllAsAscii

public open func consumeAllAsAscii(): String

功能:将所有数据转换成 Ascii String 类型。

返回值:Ascii String 类型实例

func consumeAllAsString

public open func consumeAllAsString(): String

功能:将所有数据转换成 utf8 String 类型。

返回值:utf8 String 类型实例

class DebugDataProvider

public class DebugDataProvider <: DataProvider

此类继承了 DataProvider 类型,额外增加了调试信息。

data

public let data: Array<UInt8>

功能:变异数据。

remainingBytes

public var remainingBytes: Int64

功能:剩余字节数。

offset

public var offset: Int64

功能:已转化的字节数。

func wrap

public static func wrap(dp: DataProvider): DebugDataProvider

功能:根据 DataProvider 实例创建 DebugDataProvider 实例。

参数:

  • dataDataProvider 类型实例

func consumeBool

public override func consumeBool(): Bool

功能:将数据转换成 Bool 类型实例。

返回值:Bool 类型实例

func consumeBools

public override func consumeBools(count: Int64): Array<Bool>

功能:将指定数量的数据转换成 Bool 类型数组。

参数:

  • count:指定转换的数据量

返回值:Bool 类型数组

func consumeByte

public override func consumeByte(): Byte

功能:将数据转换成 Byte 类型实例。

返回值:Byte 类型实例

func consumeBytes

public override func consumeBytes(count: Int64): Array<Byte>

功能:将指定数量的数据转换成 Byte 类型数组。

参数:

  • count:指定转换的数据量

返回值:Byte 类型数组

func consumeUInt8

public override func consumeUInt8(): UInt8

功能:将数据转换成 UInt8 类型实例。

返回值:UInt8 类型实例

func consumeUInt16

public override func consumeUInt16(): UInt16

功能:将数据转换成 UInt16 类型实例。

返回值:UInt16 类型实例

func consumeUInt32

public override func consumeUInt32(): UInt32

功能:将数据转换成 UInt32 类型实例。

返回值:UInt32 类型实例

func consumeUInt64

public override func consumeUInt64(): UInt64

功能:将数据转换成 UInt64 类型实例。

返回值:UInt64 类型实例

func consumeInt8

public override func consumeInt8(): Int8

功能:将数据转换成 Int8 类型实例。

返回值:Int8 类型实例

func consumeInt16

public override func consumeInt16(): Int16

功能:将数据转换成 Int16 类型实例。

返回值:Int16 类型实例

func consumeInt32

public override func consumeInt32(): Int32

功能:将数据转换成 Int32 类型实例。

返回值:Int32 类型实例

func consumeInt64

public override func consumeInt64(): Int64

功能:将数据转换成 Int64 类型实例。

返回值:Int64 类型实例

func consumeUInt8s

public override func consumeUInt8s(count: Int64): Array<UInt8>

功能:将指定数量的数据转换成 UInt8 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt8 类型数组

func consumeUInt16s

public override func consumeUInt16s(count: Int64): Array<UInt16>

功能:将指定数量的数据转换成 UInt16 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt16 类型数组

func consumeUInt32s

public override func consumeUInt32s(count: Int64): Array<UInt32>

功能:将指定数量的数据转换成 UInt32 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt32 类型数组

func consumeUInt64s

public override func consumeUInt64s(count: Int64): Array<UInt64>

功能:将指定数量的数据转换成 UInt64 类型数组。

参数:

  • count:指定转换的数据量

返回值:UInt64 类型数组

func consumeInt8s

public override func consumeInt8s(count: Int64): Array<Int8>

功能:将指定数量的数据转换成 Int8 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int8 类型数组

func consumeInt16s

public override func consumeInt16s(count: Int64): Array<Int16>

功能:将指定数量的数据转换成 Int16 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int16 类型数组

func consumeInt32s

public override func consumeInt32s(count: Int64): Array<Int32>

功能:将指定数量的数据转换成 Int32 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int32 类型数组

func consumeInt64s

public override func consumeInt64s(count: Int64): Array<Int64>

功能:将指定数量的数据转换成 Int64 类型数组。

参数:

  • count:指定转换的数据量

返回值:Int64 类型数组

func consumeFloat32

public override func consumeFloat32(): Float32

功能:将数据转换成 Float32 类型实例(保留接口,功能暂未实现,当前为抛出异常UnsupportException)。

返回值:Float32 类型实例

func consumeFloat64

public override func consumeFloat64(): Float64

功能:将数据转换成 Float64 类型实例(保留接口,功能暂未实现,当前为抛出异常UnsupportException)。

返回值:Float64 类型实例

func consumeChar

public override func consumeChar(): Char

功能:将数据转换成 Char 类型实例。

返回值:Char 类型实例

func consumeAsciiString

public override func consumeAsciiString(maxLength: Int64): String

功能:将数据转换成 Ascii String 类型实例。

参数:

  • maxLengthString 类型的最大长度

返回值:String 类型实例

func consumeString

public override func consumeString(maxLength: Int64): String

功能:将数据转换成 utf8 String 类型实例。

参数:

  • maxLengthString 类型的最大长度

返回值:String 类型实例

func consumeAll

public override func consumeAll(): Array<UInt8>

功能:将所有数据转换成 UInt8 类型数组。

返回值:UInt8 类型数组

func consumeAllAsAscii

public override func consumeAllAsAscii(): String

功能:将所有数据转换成 Ascii String 类型。

返回值:Ascii String 类型实例

func consumeAllAsString

public override func consumeAllAsString(): String

功能:将所有数据转换成 utf8 String 类型。

返回值:utf8 String 类型实例

class UnsupportException

public class UnsupportException <: Exception {
    public init()
    public init(message: String)
}

此异常为转换数据时,不支持该数据类型时抛出的异常。

init

public init()

功能:创建 UnsupportException 实例。

init

public init(message: String)

功能:创建 UnsupportException 实例。

参数:

  • message:异常提示信息

class ExhaustedException

public class ExhaustedException <: Exception {
    public init()
    public init(message: String)
}

此异常为转换数据时,剩余数据不足以转换时抛出的异常。

init

public init()

功能:创建 ExhaustedException 实例。

init

public init(message: String)

功能:创建 ExhaustedException 实例。

参数:

  • message:异常提示信息

示例

对字符猜测功能进行测试

  • 编写被测 API,当且仅当输入数组长度是 8、内容是 "Cangjie!" 对应的 ASCII 时抛出异常,纯随机的情况下最差需要 2**64 次猜测才会触发异常。
  • 创建 Fuzzer 并且调用待测 API,进入主流程
// 导入依赖的类
from fuzz import fuzz.Fuzzer

main() {
    // 创建Fuzzer并启动fuzz流程
    Fuzzer(api).startFuzz()
    return 0
}


// 被测函数,在满足特定条件会抛出异常,该异常会被 Fuzzer 捕获
public func api(data: Array<UInt8>): Int32 {
    if (data.size == 8 && data[0] == b'C' && data[1] == b'a' && data[2] == b'n' && data[3] == b'g' && data[4] == b'j' &&
        data[5] == b'i' && data[6] == b'e' && data[7] == b'!') {
        throw Exception("TRAP")
    }
    return 0
}

Linux 的编译命令是:cjc fuzz_main.cj --link-options="--whole-archive $CANGJIE_HOME/lib/linux_x86_64_llvm/libclang_rt.fuzzer_no_main.a -no-whole-archive -lstdc++" --sanitizer-coverage-inline-8bit-counters

macOS 的编译命令是:cjc fuzz_main.cj --link-options="$CANGJIE_HOME/lib/linux_x86_64_llvm/libclang_rt.fuzzer_no_main.a -lc++" --sanitizer-coverage-inline-8bit-counters

释义:

  • link-options 是链接时选项,fuzz 库本身依赖符号 LLVMFuzzerRunDriver,该符号需要开发者自行解决
    • 仓颉语言在 $CANGJIE_HOME/lib/linux_x86_64_llvm/libclang_rt.fuzzer_no_main.a 存放一份 修改过 的 libfuzzer,对标准的 libfuzzer 进行了增强,见 覆盖率信息打印-实验性特性
    • 可以使用 find $(clang -print-runtime-dir) -name "libclang_rt.fuzzer_no_main*.a" 寻找本地安装好的静态库文件。
  • 向 Linux 编译需要使用 whole-archive libfuzzer.a 是因为 cjc 调用 ld 后端时,从左到右顺序是 libfuzzer.alibcangjie-fuzz-fuzz.a、 libc 等基础库,该顺序会导致 libcangjie-fuzz-fuzz.a 依赖的 LLVMFuzzerRunDriver 符号未被找到。解决方案有
    • libfuzzer.a 放到 libcangjie-fuzz-fuzz.a 后面
    • 使用 whole-archive libfuzzer.a 来规避符号找不到的问题
  • -lstdc++ (Linux) / -lc++ (macOS) 用于链接 libfuzzer 依赖的 std 库
  • --sanitizer-coverage-inline-8bit-counterscjc 的编译选项,它会对当前 package 执行覆盖率反馈插桩,详见 cjc 编译器使用手册
    • 其他高级的参数有:--sanitizer-coverage-trace-compares(提高Fuzz变异的效率)、--sanitizer-coverage-pc-table(Fuzz结束后打印覆盖率信息)

libfuzzer 体验类似,可以直接运行,数秒后(取决于 CPU 性能)可获得 crash,且输入的数据是 "Cangjie!"

$ ./main
INFO: Seed: 246468919
INFO: Loaded 1 modules   (15 inline 8-bit counters): 15 [0x55bb7c76dcb0, 0x55bb7c76dcbf),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2      INITED ft: 4 corp: 1/1b exec/s: 0 rss: 28Mb
#420    NEW    ft: 5 corp: 2/9b lim: 8 exec/s: 0 rss: 28Mb L: 8/8 MS: 3 CrossOver-InsertByte-InsertRepeatedBytes-
#1323   NEW    ft: 6 corp: 3/17b lim: 14 exec/s: 0 rss: 28Mb L: 8/8 MS: 3 InsertByte-InsertByte-CrossOver-
#131072 pulse  ft: 6 corp: 3/17b lim: 1300 exec/s: 65536 rss: 35Mb
#262144 pulse  ft: 6 corp: 3/17b lim: 2600 exec/s: 65536 rss: 41Mb
#295225 NEW    ft: 7 corp: 4/25b lim: 2930 exec/s: 73806 rss: 43Mb L: 8/8 MS: 2 ShuffleBytes-ChangeByte-
#514006 NEW    ft: 8 corp: 5/33b lim: 4096 exec/s: 73429 rss: 53Mb L: 8/8 MS: 1 ChangeByte-
#524288 pulse  ft: 8 corp: 5/33b lim: 4096 exec/s: 74898 rss: 53Mb
#1048576        pulse  ft: 8 corp: 5/33b lim: 4096 exec/s: 61680 rss: 78Mb
#1064377        NEW    ft: 9 corp: 6/41b lim: 4096 exec/s: 62610 rss: 79Mb L: 8/8 MS: 1 ChangeByte-
#1287268        NEW    ft: 10 corp: 7/49b lim: 4096 exec/s: 61298 rss: 90Mb L: 8/8 MS: 1 ChangeByte-
#2097152        pulse  ft: 10 corp: 7/49b lim: 4096 exec/s: 59918 rss: 128Mb
#2875430        NEW    ft: 11 corp: 8/57b lim: 4096 exec/s: 61179 rss: 165Mb L: 8/8 MS: 2 ChangeBinInt-ChangeByte-
#4194304        pulse  ft: 11 corp: 8/57b lim: 4096 exec/s: 59918 rss: 227Mb
#4208258        NEW    ft: 12 corp: 9/65b lim: 4096 exec/s: 60117 rss: 228Mb L: 8/8 MS: 3 CrossOver-CrossOver-ChangeBit-
[WARNING]: Detect uncatched exception, maybe caused by bugs, exit now
An exception has occurred:
Exception: TRAP
         at default.api(std/core::Array<...>)(/data/Cangjie/fuzz_main.cj:14)
         at _ZN7default3apiER_ZN8std$core5ArrayIhE_cc_wrapper(/data/Cangjie/fuzz_main.cj:0)
         at libfuzzerCallback(fuzz/fuzz/callback.cj:20)
[INFO]: data is: [67, 97, 110, 103, 106, 105, 101, 33]
[INFO]: data base64: Q2FuZ2ppZSE=
crash file will stored with libfuzzer
==899957== ERROR: libFuzzer: fuzz target exited
SUMMARY: libFuzzer: fuzz target exited
MS: 1 ChangeByte-; base unit: 7d8b0108ce76a937161065eafcde95bbf3d47dbf
0x43,0x61,0x6e,0x67,0x6a,0x69,0x65,0x21,
Cangjie!
artifact_prefix='./'; Test unit written to ./crash-555e7af32a2ceb585cdd9ce810c4804e65d41cea
Base64: Q2FuZ2ppZSE=

也可以使用 -help=1 打印帮助,-seed=246468919 指定随机数的种子。

$ ./main -help=1                                                                                                                                                                                        exit 130
Usage:
To run fuzzing pass 0 or more directories.
program_name [-flag1=val1 [-flag2=val2 ...] ] [dir1 [dir2 ...] ]
To run individual tests without fuzzing pass 1 or more files:
program_name [-flag1=val1 [-flag2=val2 ...] ] file1 [file2 ...]

使用 DataProvider 功能进行测试

上文介绍了使用字节流对 API 进行测试的方法,除此之外,fuzz 包提供了 DataProvider 类,用于更友好地从变异的数据源产生仓颉的标准数据类型,方便对 API 进行测试。

public func api2(dp: DataProvider): Int32 {
    if(dp.consumeBool() && dp.consumeByte() == b'A' && dp.consumeuint32() == 0xdeadbeef){
        throw Exception("TRAP")
    }
    return 0
}

此案例中,开启 --sanitizer-coverage-trace-compares 可有效提高 fuzz 效率。

DataProvider 模式下,无法直观地判断各个 API 返回值分别是什么,因此提供了 Fuzzer.enableDebugDataProvider()DebugDataProvider,在 startFuzz 前调用 enableDebugDataProvider() 即可令本次 fuzz 每次调用 consumeXXX 时打印日志。

例如,上文代码触发异常后,添加 enableDebugDataProvider 重新编译,效果如下。

from fuzz import fuzz.*

main() {
    let fuzzer = Fuzzer(api2)
    fuzzer.enableDebugDataProvider()
    fuzzer.startFuzz()
    return 0
}
./main crash-d7ece8e77ff25769a5d55eb8d3093d4bace78e1b
Running: crash-d7ece8e77ff25769a5d55eb8d3093d4bace78e1b
[DEBUG] consumeBool return true
[DEBUG] consumeByte return 65
[DEBUG] consumeUInt32 return 3735928559
[WARNING]: Detect uncatched exception, maybe caused by bugs, exit now
An exception has occurred:
Exception: TRAP
         at default.api2(fuzz/fuzz::DataProvider)(/tmp/test.cj:12)
         at _ZN7default4api2EC_ZN9fuzz$fuzz12DataProviderE_cc_wrapper(/tmp/test.cj:0)
         at libfuzzerCallback(fuzz/fuzz/callback.cj:0)
[INFO]: data is: [191, 65, 239, 190, 173, 222]

FakeCoverage 模式

在链接了 libfuzzer <= 14 的情况下,且处于 DataProvider 模式下,遇到了类似如下的错误,可能需要阅读此章节:

ERROR: no interesting inputs were found. Is the code instrumented for coverage? Exiting.

libfuzzer 15 起,修复了该 feature,即使初始化时拒绝了输入,也不会停止执行。

注意:请确认被测试的库确实被插入了覆盖率反馈,因为在没有覆盖率反馈插桩的情况下,也会出现该错误!

当前 fuzz 后端对接到了 libfuzzer,而 libfuzzer 在启动时会先输入空字节流、再输入仅包含一个 '\n' 的字节流对待测函数进行试探,在两轮结束后检测覆盖率是否新增。在 DataProvider 模式下,如果先消耗数据,再调用待测库的 API,会导致消耗数据时长度不足而提前返回,从而 libfuzzer 认为覆盖率信息为零。

例如下方代码,会触发该错误

触发的代码:

// main.cj
from fuzz import fuzz.*

main() {
    let f = Fuzzer(api)
    f.disableFakeCoverage()
    f.startFuzz()
    return 0
}

// fuzz_target.cj, with sancov
public func api(dp: DataProvider): Int32 {
    if (dp.consumeBool() && dp.consumeBool()) {
        throw Exception("TRAP!")
    }
    return 0
}

因此,需要使用 Fake Coverage 创建虚假的覆盖率信息,让 libfuzzer 在初始化期间认为待测模块确实被插桩,等到 DataProvider 收集到足够数据后,再进行有效的 fuzz 测试。该模式被称为 Fake Coverage 模式。

将上文的 disableFakeCoverage() 替换为 enableFakeCoverage() 即可继续运行,最终触发 TRAP。

此外,除了使用 Fake Coverage 模式,还可以在测试用例中主动调用待测函数的某些不重要的API来将覆盖率信息传递给 libfuzzer,也能起到让 fuzz 继续下去的作用。

栈回溯缺失的处理方案

WARNING: Failed to find function "__sanitizer_acquire_crash_state".
WARNING: Failed to find function "__sanitizer_print_stack_trace".
WARNING: Failed to find function "__sanitizer_set_death_callback".

在启动 fuzz 时默认会有这三条 WARNING,因为当前 cj-fuzz 没有对它们进行实现。在 fuzz 过程中,可能会因为

  • 抛出异常
  • 超时
  • 在 C 代码中 crash

而结束 fuzz 流程。

其中“抛出异常”的情况,fuzz 框架对异常进行捕获后会打印栈回溯,不会造成栈回溯缺失的现象。

“超时”和“在 C 代码中 crash”实际是在 native 代码中触发了 SIGNAL,不属于仓颉异常,因此会造成栈回溯的缺失。

libfuzzer 会尝试使用 __sanitizer_acquire_crash_state__sanitizer_print_stack_trace__sanitizer_set_death_callback 等函数处理异常情况,其中 __sanitizer_print_stack_trace 会打印栈回溯,目前成熟的实现在 llvm compiler-rt 中的 asan 等模块中。

因此,建议的解决方案是在链接时额外加入如下的静态库文件和链接选项,释义如下

/usr/lib/llvm-14/lib/clang/14.0.0/lib/linux/libclang_rt.asan-x86_64.a -lgcc_s --eh-frame-hdr

  • /usr/lib/llvm-14/lib/clang/14.0.0/lib/linux/libclang_rt.asan-x86_64.a 因为该 .a 文件实现了 __sanitizer_print_stack_trace,出于方便就直接用它。
  • -lgcc_s 栈回溯依赖 gcc_s
  • --eh-frame-hdr ld 链接时生成 eh_frame_hdr 节,帮助完成栈回溯

可选的环境变量:ASAN_SYMBOLIZER_PATH=$CANGJIE_HOME/third_party/llvm/bin/llvm-symbolizer,可能在某些情况下有用。

最终会得到两套栈回溯,一套是 Exception.printStackTrace,一套是 __sanitizer_print_stack_trace,内容如下:

[WARNING]: Detect uncatched exception, maybe caused by bugs, exit now
An exception has occurred:
Exception: TRAP!
         at default.ttt(std/core::Array<...>)(/data/cangjie/libs/fuzz/ci_fuzzer0.cj:11)
         at _ZN7default3tttER_ZN8std$core5ArrayIhE_cc_wrapper(/data/cangjie/libs/fuzz/ci_fuzzer0.cj:0)
         at libfuzzerCallback(/data/cangjie/libs/fuzz/fuzz/callback.cj:34)
[INFO]: data is: [0, 202, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
[INFO]: crash file will stored with libfuzzer
==425243== ERROR: libFuzzer: fuzz target exited
    #0 0x563a233fadf1 in __sanitizer_print_stack_trace (/data/cangjie/libs/fuzz/main+0x280df1)
    #1 0x563a2337c0b8 in fuzzer::PrintStackTrace() (/data/cangjie/libs/fuzz/main+0x2020b8)
    #2 0x563a2338726c in fuzzer::Fuzzer::ExitCallback() (/data/cangjie/libs/fuzz/main+0x20d26c)
    #3 0x7f485cf36494 in __run_exit_handlers stdlib/exit.c:113:8
    #4 0x7f485cf3660f in exit stdlib/exit.c:143:3
    #5 0x563a23224e68 in libfuzzerCallback$real /data/cangjie/libs/fuzz/fuzz/callback.cj:62:18
    #6 0x7f485d22718b in CJ_MCC_N2CStub (/data/cangjie/output/runtime/lib/linux_x86_64_llvm/libcangjie-runtime.so+0x2718b)
    #7 0x563a2322fc26 in libfuzzerCallback /data/cangjie/libs/fuzz/fuzz/callback.cj:20
    #8 0x563a23387883 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/data/cangjie/libs/fuzz/main+0x20d883)
    #9 0x563a2338a3f9 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/data/cangjie/libs/fuzz/main+0x2103f9)
    #10 0x563a23387e49 in fuzzer::Fuzzer::MutateAndTestOne() (/data/cangjie/libs/fuzz/main+0x20de49)
    #11 0x563a2338a2b5 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) (/data/cangjie/libs/fuzz/main+0x2102b5)
    #12 0x563a23377a12 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/data/cangjie/libs/fuzz/main+0x1fda12)
    #13 0x563a231ad2b6 in fuzz_fake$fuzz::Fuzzer::startFuzz() /data/cangjie/libs/fuzz/fuzz/fuzzer.cj:200:13
    #14 0x563a23405fad in default::main() /data/cangjie/libs/fuzz/ci_fuzzer0.cj:5:5
    #15 0x563a23405fe7 in user.main /data/cangjie/libs/fuzz/<stdin>
    #16 0x563a234060e1 in cj_entry$ (/data/cangjie/libs/fuzz/main+0x28c0e1)
    #17 0x7f485d227220  (/data/cangjie/output/runtime/lib/linux_x86_64_llvm/libcangjie-runtime.so+0x27220)
    #18 0x7f485d223898  (/data/cangjie/output/runtime/lib/linux_x86_64_llvm/libcangjie-runtime.so+0x23898)
    #19 0x7f485d2607b9 in CJ_CJThreadEntry (/data/cangjie/output/runtime/lib/linux_x86_64_llvm/libcangjie-runtime.so+0x607b9)

覆盖率信息打印 实验性特性

仓颉 fuzzer 支持使用 -print_coverage=1 作为启动参数运行 fuzzer,用于统计各函数的测试情况,该特性在持续完善中,只与输出覆盖率报告有关,不影响 fuzz 过程。

由于该功能需要对 libfuzzer 进行侵入式修改,使用该功能需要链接仓颉自带的 libfuzzer,路径是:$CANGJIE_HOME/lib/{linux_x86_64_llvm, linux_aarch64_llvm}/libclang_rt-fuzzer_no_main.a。

编译时需要同时启用--sanitizer-coverage-inline-8bit-counters--sanitizer-coverage-pc-table

C 语言 libfuzzer 输出举例

./a.out -print_coverage=1
COVERAGE:
COVERED_FUNC: hits: 5 edges: 6/8 LLVMFuzzerTestOneInput /tmp/test.cpp:5
  UNCOVERED_PC: /tmp/test.cpp:6
  UNCOVERED_PC: /tmp/test.cpp:9

仓颉语言 cj-fuzz 输出举例

./main -print_coverage=1 -runs=100
Done 100 runs in 0 second(s)
COVERAGE:
COVERED_FUNC: hits: 1 edges: 3/12 ttt <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
  UNCOVERED_PC: <unknown cj filename>:<unknown cj line number>
UNCOVERED_FUNC: hits: 0 edges: 0/2 main <unknown cj filename>:<unknown cj line number>
COVERED_FUNC: hits: 1 edges: 1/1 ttt_cc_wrapper <unknown cj filename>:<unknown cj line number>

cjvm 使用 cj-fuzz 功能

差异说明:cjvm 无法加载 .a 文件,只能加载 .so 文件。

cj-fuzz 运行时依赖 libclang_rt.fuzzer_no_main.alibcangjie-fuzz-fuzzFFI.a,因此需要将它们链接为动态链接库的格式,命令如下:

clang++ -shared -Wl,--whole-archive libclang_rt.fuzzer_no_main.a ${CANGJIE_HOME}/lib/linux_x86_64_jet/libcangjie-fuzz-fuzzFFI.a -Wl,--no-whole-archive -o libcangjie-fuzz-fuzzFFI.so

运行 cj-fuzz

  1. 通过上述命令,获得 libcangjie-fuzz-fuzzFFI.so
  2. cjc fuzz_main.cj --sanitizer-coverage-inline-8bit-counters ${CANGJIE_HOME}/modules/linux_x86_64_jet/fuzz/fuzz.bc -lcangjie-fuzz-fuzzFFI
    1. --sanitizer-coverage-inline-8bit-counters 启用覆盖率插桩
    2. ${CANGJIE_HOME}/modules/linux_x86_64_jet/fuzz/fuzz.bc 主动链接 fuzz 模块的 fuzz 包的字节码
    3. -lcangjie-fuzz-fuzzFFI 指定依赖库的名称,运行时会搜索 libcangjie-fuzz-fuzzFFI.so 进行动态加载
  3. LD_LIBRARY_PATH=/path/to/lib:${LD_LIBRARY_PATH} cj main.cbc
    1. 按需修改 LD_LIBRARY_PATH
    2. 执行 cbc 文件

实际效果如下:

cp /usr/lib/llvm-14/lib/clang/14.0.0/lib/linux/libclang_rt.fuzzer_no_main-x86_64.a .
clang++ -shared -Wl,--whole-archive libclang_rt.fuzzer_no_main-x86_64.a ${CANGJIE_HOME}/lib/linux_x86_64_jet/libcangjie-fuzz-fuzzFFI.a -Wl,--no-whole-archive -o libcangjie-fuzz-fuzzFFI.so
cjc --sanitizer-coverage-inline-8bit-counters fuzz_main.cj ${CANGJIE_HOME}/modules/linux_x86_64_jet/fuzz/fuzz.bc -lcangjie-fuzz-fuzzFFI
LD_LIBRARY_PATH=/path/to/lib:${LD_LIBRARY_PATH} cj main.cbc
>>>>
    WARNING: Failed to find function "__sanitizer_acquire_crash_state".
    WARNING: Failed to find function "__sanitizer_print_stack_trace".
    WARNING: Failed to find function "__sanitizer_set_death_callback".
    INFO: Running with entropic power schedule (0xFF, 100).
    INFO: Seed: 3156944264
    INFO: Loaded 1 modules   (21 inline 8-bit counters): 21 [0x5627041690a0, 0x5627041690b5),
    INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
    INFO: A corpus is not provided, starting from an empty corpus
    #2      INITED ft: 4 corp: 1/1b exec/s: 0 rss: 52Mb
    #488    NEW    ft: 5 corp: 2/9b lim: 8 exec/s: 0 rss: 53Mb L: 8/8 MS: 1 InsertRepeatedBytes-
    #12303  NEW    ft: 6 corp: 3/17b lim: 122 exec/s: 0 rss: 54Mb L: 8/8 MS: 5 CrossOver-ChangeBit-ShuffleBytes-ShuffleBytes-ChangeByte-
    #20164  NEW    ft: 7 corp: 4/25b lim: 198 exec/s: 0 rss: 54Mb L: 8/8 MS: 1 ChangeByte-
    #180030 NEW    ft: 8 corp: 5/33b lim: 1780 exec/s: 180030 rss: 55Mb L: 8/8 MS: 1 ChangeByte-
    #524288 pulse  ft: 8 corp: 5/33b lim: 4096 exec/s: 174762 rss: 55Mb
    #671045 NEW    ft: 9 corp: 6/41b lim: 4096 exec/s: 167761 rss: 55Mb L: 8/8 MS: 5 InsertByte-ChangeByte-ChangeBit-ChangeByte-EraseBytes-
    #758816 NEW    ft: 10 corp: 7/49b lim: 4096 exec/s: 151763 rss: 55Mb L: 8/8 MS: 1 ChangeByte-
    #1048576        pulse  ft: 10 corp: 7/49b lim: 4096 exec/s: 149796 rss: 55Mb
    #1947938        NEW    ft: 11 corp: 8/57b lim: 4096 exec/s: 162328 rss: 55Mb L: 8/8 MS: 2 InsertByte-EraseBytes-
    #2097152        pulse  ft: 11 corp: 8/57b lim: 4096 exec/s: 161319 rss: 55Mb
    #3332055        NEW    ft: 12 corp: 9/65b lim: 4096 exec/s: 151457 rss: 55Mb L: 8/8 MS: 2 ChangeByte-ChangeBit-
    [WARNING]: Detect uncatched exception, maybe caused by bugs, exit now
    An exception has occurred:
    Exception: TRAP
             at default.api(/cjvm_demo/test.cj:20)
             at default.api(/cjvm_demo/test.cj:0)
             at fuzz/fuzz.libfuzzerCallback(/cangjie/lib/src/fuzz/fuzz/callback.cj:34)
             at fuzz/fuzz.Fuzzer.startFuzz(/cangjie/lib/src/fuzz/fuzz/fuzzer.cj:223)
             at default.<main>(/cjvm_demo/test.cj:5)
             at default.user.main(<unknown>:0)
    [INFO]: data is: [67, 97, 110, 103, 106, 105, 101, 33]
    [INFO]: crash file will stored with libfuzzer
    ==33946== ERROR: libFuzzer: fuzz target exited
    SUMMARY: libFuzzer: fuzz target exited
    MS: 1 ChangeByte-; base unit: 1719c2c0bbc676f5b436528c183e4743a455d66a
    0x43,0x61,0x6e,0x67,0x6a,0x69,0x65,0x21,
    Cangjie!
    artifact_prefix='./'; Test unit written to ./crash-555e7af32a2ceb585cdd9ce810c4804e65d41cea
    Base64: Q2FuZ2ppZSE=