记一次 fuzz go 的一个第三方库

提交 PR 并被 merge

Posted by pic4xiu on May 31, 2023

项目一览

偶然间看到的库

这个项目是将 PNG、JPEG 和 GIF 编码的图形文件转换为 ZPL 兼容的 ^GF 元素,然后作者给出了实例代码:

package main

import (
    "simonwaldherr.de/go/zplgfa"
    "fmt"
    "image"
    _ "image/gif"
    _ "image/jpeg"
    _ "image/png"
    "log"
    "os"
)

func main() {
    // open file
    file, err := os.Open("label.png")
    if err != nil {
        log.Printf("Warning: could not open the file: %s\n", err)
        return
    }

    defer file.Close()

    // load image head information
    config, format, err := image.DecodeConfig(file)
    if err != nil {
        log.Printf("Warning: image not compatible, format: %s, config: %v, error: %s\n", format, config, err)
    }

    // reset file pointer to the beginning of the file
    file.Seek(0, 0)

    // load and decode image
    img, _, err := image.Decode(file)
    if err != nil {
        log.Printf("Warning: could not decode the file, %s\n", err)
        return
    }

    // flatten image
    flat := zplgfa.FlattenImage(img)

    // convert image to zpl compatible type
    gfimg := zplgfa.ConvertToZPL(flat, zplgfa.CompressedASCII)

    // output zpl with graphic field data to stdout
    fmt.Println(gfimg)
}

整体跑一遍基本就能明白他在干什么。然后把代码改成 fuzz 的格式:

package test

import (
	"bytes"
	"fmt"
	"image/gif"
	"testing"

	"simonwaldherr.de/go/zplgfa"
)

func FuzzReverse(f *testing.F) {
	f.Fuzz(func(a *testing.T, data []byte) { //接收参数

		// load and decode image
		img, err := gif.Decode(bytes.NewReader(data))
		if err != nil {
			return
		}

		// flatten image
		flat := zplgfa.FlattenImage(img)
		// wid := flat.Bounds().Size().X / 8

		// panic(wid)
		// convert image to zpl compatible type
		gfimg := zplgfa.ConvertToZPL(flat, zplgfa.CompressedASCII)

		// output zpl with graphic field data to stdout
		fmt.Println(gfimg)
	})
}

直接跑一遍就有 bug 了:

❯ go test -fuzz=Fuzz
fuzz: elapsed: 2s, gathering baseline coverage: 0/1541 completed
fuzz: minimizing 220-byte failing input file
fuzz: elapsed: 2s, gathering baseline coverage: 434/1541 completed
--- FAIL: FuzzReverse (1.93s)
    --- FAIL: FuzzReverse (0.00s)
        testing.go:1485: panic: runtime error: index out of range [0] with length 0
            goroutine 1254 [running]:
            runtime/debug.Stack()
                /usr/local/go/src/runtime/debug/stack.go:24 +0xbc
            testing.tRunner.func1()
                /usr/local/go/src/testing/testing.go:1485 +0x264
            panic({0x102b9e940, 0x14000016318})
                /usr/local/go/src/runtime/panic.go:884 +0x204
            simonwaldherr.de/go/zplgfa.ConvertToGraphicField({0x102bb0de8, 0x140000a8a00}, 0x2)
                /Users/*/go/pkg/mod/simonwaldherr.de/go/zplgfa@v1.1.1/zplgfa.go:160 +0xb44
            simonwaldherr.de/go/zplgfa.ConvertToZPL({0x102bb0de8?, 0x140000a8a00?}, 0x140000d9748?)
                /Users/*/go/pkg/mod/simonwaldherr.de/go/zplgfa@v1.1.1/zplgfa.go:31 +0x50
            test.FuzzReverse.func1(0x140000d9718?, {0x14000026700, 0x1f, 0x80})
                /Users/*/Desktop/src/cve/m_test.go:27 +0x130
            reflect.Value.call({0x102b7c560?, 0x102baeca0?, 0x14000544e38?}, {0x102b27556, 0x4}, {0x1400009b8f0, 0x2, 0x0?})
                /usr/local/go/src/reflect/value.go:586 +0x87c
            reflect.Value.Call({0x102b7c560?, 0x102baeca0?, 0x0?}, {0x1400009b8f0?, 0x102bade20?, 0x102c781f8?})
                /usr/local/go/src/reflect/value.go:370 +0x90
            testing.(*F).Fuzz.func1.1(0x0?)
                /usr/local/go/src/testing/fuzz.go:335 +0x360
            testing.tRunner(0x1400010b040, 0x1400007ad80)
                /usr/local/go/src/testing/testing.go:1576 +0x10c
            created by testing.(*F).Fuzz.func1
                /usr/local/go/src/testing/fuzz.go:322 +0x4c4
            
    
    Failing input written to testdata/fuzz/FuzzReverse/3e80f54c43de28a6
    To re-run:
    go test -run=FuzzReverse/3e80f54c43de28a6
FAIL
exit status 1
FAIL    test    2.559s

进代码里看看:发现在 ConvertToGraphicField 函数中,使用 image 库的 Bounds().Size() 获取了图片的长宽信息,但是事实上,这里是可以伪造的

可以看到这里的宽度就是0,之后顺着代码往下读,发现在下边的for循环中,line是根据宽度make出来的数组,而之后求currentByte时候更是直接求了索引为0的byte,这显然是个索引越界

func ConvertToGraphicField(source image.Image, graphicType GraphicType) string {
    var gfType string
    var lastLine string
    size := source.Bounds().Size()
    width := size.X / 8
    height := size.Y
    if size.Y%8 != 0 {
        width = width + 1
    }


    var GraphicFieldData string


    for y := 0; y < size.Y; y++ {
        line := make([]uint8, width)
        lineIndex := 0
        index := uint8(0)
        currentByte := line[lineIndex]//line[0]!

修复

我的修复思路就是越早处理越好,顺着调用链找,看看哪里返回是最合适的:其实这里就可以直接返回了(我认为

func ConvertToZPL(img image.Image, graphicType GraphicType) string {
    wid := img.Bounds().Size().X / 8
    if wid == 0 {
        return ""
    }
    return fmt.Sprintf("^XA,^FS\n^FO0,0\n%s^FS,^XZ\n", ConvertToGraphicField(img, graphicType))
}

因为宽度为零的话事实上已经可以直接把空字符串返回了,之所以没有返回一个 err 是因为这个师傅写的代码,整体都没用到 err,变参数对整体影响太大了,而且返回""也是合理的,因为图片本身宽度就是 0,0 确实可以代表没有图片信息。

师傅果断进行了合并: