เรียนรู้เรื่อง fuzzing ใน Go
Tags: go
Published: 2022-03-27 12:00:11

ต้นทางมาจาก [Tutorial](https://go.dev/doc/tutorial/fuzz) ของ Go อีกครั้งนะครับเกี่ยวกับเรื่อง fuzzing นี้

ว่าด้วยเรื่องพื้นฐานการทำ fuzzing ใน Go โดยหน้าที่ของมันคือการ random ข้อมูลเพื่อเอามาใส่ใน test ที่เราเขียนขึ้นมานั่นเอง

ถ้าถามว่าทำไปทำไป ก็ทำไปเพื่อค้นหาจุดอ่อน ว่าถ้ามี input แปลกๆที่เราไม่เคยคิดถึงมาก่อน หลุดเข้าไปในฟังก์ชันที่เราเขียนขึ้นมาแล้ว มันจะมีอาการผิดปกติยังไง

ตัวอย่างคลาสสิคของสิ่งที่ fuzzing หาเจอได้ก็ยกตัวอย่างเช่นพวก SQL injection, buffer overflow, DoS และพวก cross-site scripting

ในบทเรียนนี้เราจะลองมาเขียนฟังก์ชันเทสง่ายๆด้วยการใช้ fuzzing กัน จากนั้นมาทดลองดูว่ามันฟ้องอะไรเราออกมา แล้วจะแก้ไขให้มันดีขึ้นได้อย่างไร

ในกรณีที่พบคำศัพท์ที่ไม่คุ้น ให้ไปดูที่เรื่อง [Go Fuzzing glossary](https://go.dev/doc/fuzz/#glossary)

ในเนื้อหานี้เราจำมีขั้นตอนดังนี้

1. สร้างโฟลเดอร์สำหรับเขียนโค้ด
2. เพิ่ม code สำหรับการทดสอบ
3. เพิ่ม unit test
4. เพิ่ม fuzz test
5. แก้บั๊ค
6. สำรวจแหล่งข้อมูลเพิ่มเติม

**Note**: ในเวอร์ชั่นปัจจุบันจะรองรับเฉพาะ built-in types ตามเอกสารนี้ [Go Fuzzing docs](https://go.dev/doc/fuzz/#requirements) เท่านั้น ส่วนที่นอกเหนือจากนี้จะถูกเพิ่มในอนาคต

## สิ่งที่ต้องเตรียม

- Go 1.18 หรือใหม่กว่า
- Editor ที่ชอบที่ชอบ
- command terminal ตาม OS และตัวที่ชอบ
- สิ่งแวดล้อมที่รองรับการทำ fuzznig ซึ่งปัจจุบันจะมีแค่ AMD64 และ ARM64

## สร้างโฟลเดอร์สำหรับเขียนโค้ด

ก่อนจะเริ่มเขียนโค้ด ก็ต้องสร้างโฟลเดอร์กันก่อน

1. เปิด command prompt และไปที่ home directory

บน Linux หรือ Mac:

```sh
$ cd
```

บน Windows:

```sh
C:\> cd %HOMEPATH%
```

ต่อจากนี้จะแสดงเฉพาะ $ prompt เพราะว่าคำสั่งที่เหลือจะทำงานบน Windows ได้ด้วย

2. จาก command prompt ให้สร้างไดเร็กทอรี่ชื่อ fuzz

```sh
$ mkdir fuzz
$ cd fuzz
```

3. สร้าง module ให้โค้ดของเราก่อน

ใช้คำสั่ง go mod init ตามนี้

```sh
$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz
```

## เพิ่ม code สำหรับการทดสอบ

ในขั้นตอนนี้เราจะมาเพิ่มฟังก์ชัน reverse ให้ string

### เขียนโค้ด

1. ใช้ text editor สร้างไฟล์ชื่อ main.go ในไดเร็คทอรี่ fuzz
2. ในไฟล์ main.go ให้ประกาศ package ไว้บนสุดตามนี้

```go
package main
```

3. ต่อจากนั้นให้วางฟังก์ชันนี้ตามลงไป

```go
func Reverse(s string) string {
    b := []byte(s)
    for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}
```

ฟังก์ชันนี้จะรับ string มาวนลูป byte ทีละตัว และสุดท้ายจะคืน string ที่ย้อนศรลำดับจากหลังมาหน้าให้

4. ต่อมาให้เพิ่มการเรียกใช้ฟังก์ชันลงไปในฟังก์ชัน main ตามนี้ โดยเริ่มจากพิมพ์ประโยคตั้งต้น แล้วย้อนมันครั้งหนึ่ง พิมพ์ออกมา แล้วก็ย้อนอีกรอบ

```go
func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev := Reverse(input)
    doubleRev := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q\n", rev)
    fmt.Printf("reversed again: %q\n", doubleRev)
}
```

ฟังก์ชันนี้จะพิมพ์ผลลัพธ์ออกมาให้เราเห็นบน command line จะได้ดีบั๊คง่ายหน่อย

5. ฟังก์ชัน main มีการใช้ fmt งั้นเราก็เลยต้องมา import มันด้วย เราจะก็จะได้โค้ดบรรทัดแรกหน้าตาแบบนี้

```go
package main

import "fmt"
```

### รันโค้ด

ให้รันคำสั่งนี้บน command line ตรงที่ main.go อยู่

```sh
$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"
```

เราจะเห็นประโยคต้นฉบับ และผลของการย้อนประโยคครั้งหนึ่ง และย้อนอีกครั้งเพื่อให้กลับไปเหมือนต้นฉบับ

เอาละ ได้เวลาไปเขียนเทสกันซักที

## เพิ่ม unit test

ในขั้นตอนนี้เราจะเขียน unit test ตามแบบปกติที่ทำกันให้กับฟังก์ชัน Reverse

### เขียนโค้ด

1. ใช้ text editor สร้างไฟล์ชื่อ reverse_test.go ในไดเร็คทอรี่ fuzz
2. วางโค้ดนี้ลงไปใน reverse_test.go

```go
package main

import (
    "testing"
)

func TestReverse(t *testing.T) {
    testcases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {" ", " "},
        {"!12345", "54321!"},
    }
    for _, tc := range testcases {
        rev := Reverse(tc.in)
        if rev != tc.want {
                t.Errorf("Reverse: %q, want %q", rev, tc.want)
        }
    }
}
```

เทสนี้จะทำการยืนยันว่าผลของการ reverse นั้นถูกต้อง

### รันโค้ด

รัน unit test ด้วยคำสั่ง go test

```sh
$ go test
PASS
ok      example/fuzz  0.013s
```

เรียบร้อย ขั้นตอนต่อไปเราจะไปเทสด้วย fuzz test กันดู

## เพิ่ม fuzz test

การเขียนแค่ unit test มันมีข้อจำกัด กล่าวคือ input แต่ละตัวของการเทสที่เพิ่มเข้าไปต้องทำโดย developer เท่านั้น ส่วนการทำ fuzzing จะมาช่วยอุดรูรั่วตรงนี้ ด้วยการสร้าง input อื่นๆขึ้นมาทำการทดสอบ ซึ่งจะช่วยให้เราอาจจะเจอกรณีแปลกที่คาดไม่ถึง

ตอนนี้เราจะมาเปลี่ยน unit test ปกติให้มาเทสแบบ fuzz test ซึ่งจะช่วยให้เราทดสอบด้วย input ที่หลากหลายมากขึ้นกัน

Note: เราสามารถเก็บ unit test หรือ benchmark และ fuzz เอาไว้ในไฟล์ *_test.go ด้วยกันได้ เพียงแค่ในตัวอย่างนี้เราจะทำการเปลี่ยนมันให้ดูแค่นั้น

### เขียนโค้ด

ใน text editor ให้เราแทนที่เทสเดิมใน reverse_test.go ด้วยโค้ดชุดใหม่ตามนี้

```go
func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev := Reverse(orig)
        doubleRev := Reverse(rev)
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}
```

Fuzzing เองก็มีข้อจำกัดอยู่บ้าง ก็คือว่าในกรณีการเขียน unit test ของเรา เราสามารถคาดเดาผลลัพธ์ที่ได้จากฟังก์ชัน Reverse ได้โดยตรง ว่ามันควรจะได้ผลเช่นไร

เช่นในกรณีที่เราทดสอบด้วย `Reverse("Hello, world")` เราก็คาดว่าจะได้ผลลัพธ์ออกมาเป็น "dlrow ,olleH" ได้เลย

แต่ใน fuzzing เราไม่สามารถคาดเดาผลลัพธ์แบบนั้นได้ นั่นเพราะว่าเราไม่ได้เป็นคนควบคุมตัว input ที่จะใส่ลงไป

อย่างไรก็ดี ก็ยังมีคุณสมบัติบางอย่างที่เราเอามาตรวจสอบได้ในกรณี fuzz test ซึ่งก็มี 2 คุณสมบัติที่เราตรวจสอบในเทสนี้คือ

1. เมื่อเรา reverse มันสองครั้ง มันควรจะได้ประโยคเดิม
2. ค่าที่ reverse ยังคงอยู่ในรูปแบบของ UTF-8 ที่ถูกต้อง

**Note**: วิธีเขียน unit test กับ fuzz test มี sysntax ที่ต่างกันอยู่คือ:

- ฟังก์ชันที่เทสเปลี่ยนจากการขึ้นต้นคำว่า TestXxx มาเป็น FuzzXxx แทน และการรับพารามิเตอร์ก็เปลี่ยนจาก *testing.T มาเป็น *testing.F แทน
- เปลี่ยนการเขียนจากการใช้ t.Run มาเป็น f.Fuzz แทน โดยมันจะรับเอาฟังก์ชั่นเป้าหมายที่จะทำการ fuzz เข้ามา โดยเจ้าฟังก์ชันนั้นมีพารามิเตอร์เป็น *testing.T และ type ที่จะถูกทำ fuzz ส่วน input ของ unit test นั้นถูกกำหนดเป็น seed corpus ด้วยคำสั่ง f.Add

อย่าลืมตรวจสอบว่าได้ import `unicode/utf8` เข้ามาแล้ว

```go
package main

import (
    "testing"
    "unicode/utf8"
)
```

ตอนนี้เราเปลี่ยนมาเป็น fuzz test เรียบร้อย ได้เวลาไปรันเทสอีกครั้งละ

### รันโค้ด

1. ทดลองรัน fuzz test โดยไม่ทำ fuzzing เพื่อให้มันใจว่า seed ของเราทำงานได้ก่อนสักที

```sh
$ go test
PASS
ok      example/fuzz  0.013s
```

เราสามารถรันเทสด้วยคำสั่ง `go test -run=FuzzReverse` ในกรณีที่มีเทสอื่นที่เราไม่อยากเทสอยู่ด้วย

2. รัน FuzzReverse ด้วย fuzzing เพื่อตรวจสอบการสร้าง input แบบสุ่มเข้าไปดูบ้างว่าจะมีอะไรผิดพลาดเกิดขึ้น ซึ่งคราวนี้เราจะใช้ flag ใหม่ -fuzz ร่วมด้วย

```sh
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: minimizing 38-byte failing input file...
--- FAIL: FuzzReverse (0.01s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"

    Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
    To re-run:
    go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
FAIL
exit status 1
FAIL    example/fuzz  0.030s
```

การความผิดพลาดเมื่อทำ fuzzing และ input ที่เป็นต้นเหตุของปัญหานี้จะถูกเขียนลงไปในไฟล์ seed corpus ซึ่งมันจะถูกรันในการสั่ง go test ครั้งถักไป แม้ว่าจะไม่ใส่ flag -fuzz ก็ตาม และถ้าอยากเห็น input นั้น ก็สามารถเปิดไฟล์ดูได้ที่ไดเร็คทอรี่ `testdata/fuzz/FuzzReverse` ใน text editor ได้เลย ซึ่งข้อมูลในไฟล์นั้นอาจจะมีข้อความไม่เหมือนแบบนี้ แต่จะหน้าตาประมาณนี้

```sh
go test fuzz v1
string("泃")
``` 

โดยบรรทัดแรกจะบอกเวอร์ชัน หลังจากนั้นจะตามมาด้วยค่าแต่ละค่าตาม type และจำนวน input

3. ลองรัน go test อีกรอบโดยไม่ใส่ -fuz ดูจะพบว่า seed corpus จะถูกนำมาใช้ด้วย

```sh
$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
        reverse_test.go:20: Reverse produced invalid string
FAIL
exit status 1
FAIL    example/fuzz  0.016s
```

ตอนนี้เราทำเทสพังซะแล้ว ถึงเวลาต้องแก้บั๊กกันซะที

## ทำการแก้ไข error

ตอนนี้เราจะทำการ debug และแก้บั๊กกัน

ตอนนี้อยากให้คุณใช้เวลาคิดด้วยตัวเองก่อน ว่าจะแก้ข้อผิดพลาดนี้ยังไงดี อยากใช้เวลานานแค่ไหนก็ได้นะ เสร็จแล้วค่อยไปต่อ

### ถึงเวลาวิเคราะห์ปัญหา

ที่จริงก็มีหลายวิธีในการทำ debug ซึ่งถ้าเราใช้ VS Code อยู่แล้ว เราสามารถ[ติดตั้งตัว debugger](https://github.com/golang/vscode-go/blob/master/docs/debugging.md) เพื่อใช้วิเคราะห์ปัญหากัน แต่ในบทเรียนนี้ เราจะใช้ log ลงไปใน terminal กันเลย

ก่อนอื่น เรามาพิจารณาเอกสารอ้างอิงของ utf8.ValidString กันก่อน

> ValidString reports whether s consists entirely of valid UTF-8-encoded runes.

โดยเจ้าฟังก์ชัน Reverse ของเราตอนนี้มันทำการย้อนตัวอักษรแบบ byte ต่อ byte กันเลย และนั้นก็คือปัญหาล่ะ เพราะโดยปกติ string ที่เป็น UTF-8 มันคือ rune เพราะฉะนั้นเราจะต้องแก้ให้มันย้อนตัวอักษรแบบ rune ต่อ rune แทน

ก่อนอื่นเราคงจะต้องมาพิจารณากันอีกสักหน่อยก่อนว่าทำไม input (ในกรณีนี้เป็นอักษรภาษาจีน 泃) ถึงได้สร้างปัญหาให้ Reverse เกิดความผิดพลาดเมื่อย้อนอักษรได้ เราจะทำการตรวจสอบจำนวนของ rune ของคำที่ย้อนกลับแล้ว

### เขียนโค้ด

ใน text editor ให้แทนที่ FuzzReverse ด้วยโค้ดชุดนี้

```go
f.Fuzz(func(t *testing.T, orig string) {
    rev := Reverse(orig)
    doubleRev := Reverse(rev)
    t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
    if orig != doubleRev {
        t.Errorf("Before: %q, after: %q", orig, doubleRev)
    }
    if utf8.ValidString(orig) && !utf8.ValidString(rev) {
        t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    }
})
```

ตอนนี้ t.Logf จะพิมพ์ข้อความไปใน command line หากเกิด error และถ้ายิ่งเพิ่ม -v เข้าไปด้วยจะช่วยให้เห็นข้อผิดพลาดตรงจุดยิ่งขึ้น

### รันโค้ด

รันเทสด้วยคำสั่ง go test

```sh
$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
        reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
FAIL
exit status 1
FAIL    example/fuzz    0.598s
```

ในขณะที่ seed corpus ที่เราใส่ไปนั้น มันเป็นตัวอักษรละไบต์เท่าั้น แต่อักษร 泃 มันต้องการมากกว่าหนึ่งไบต์เพื่อสร้างมันขึ้นมา ดังนั้นการย้อนกับตัวอักษรแบบ ไบต์ต่อไบต์ก็เลยทำให้การย้อนอักษรหลายไบต์ทำงานผิดพลาดนั่นเอง

เพื่อทำความเข้าใจให้มากกว่านี้ เราจะมาแก้ข้อผิดพลาดนี้ด้วยกัน

## แก้ Error

เพื่อทำให้การทำงานถูกต้อง เรามาเปลี่ยน string ให้เป็น runes แทนที่จะใช้ bytes กัน

### เขียนโค้ด

ใน text editor ให้แทนที่ฟังก์ชัน Reverse ตามนี้

```go
func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}
```

จุดต่างที่สำคัญก้คือตอนนี้ Reverse ใช้วิธีวนแต่ละรอบของอักษรใน string ทีละ rune แทน byte

### รันโค้ด

1. รันเทสด้วยคำสั่ง go test

```sh
$ go test
PASS
ok      example/fuzz  0.016s
```

ตอนนี้เทสผ่านแล้ว!

2. เทส Fuzz อีกทีด้วยคำสั่ง go test -fuzz เพื่อตรวจหาบั๊กใหม่

```sh
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
fuzz: minimizing 506-byte failing input file...
fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
--- FAIL: FuzzReverse (0.02s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:33: Before: "\x91", after: "�"

    Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
    To re-run:
    go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
FAIL
exit status 1
FAIL    example/fuzz  0.032s
```

เราจะได้เห็นว่าเมื่อทำการย้อนกลับข้อความซ้ำอีกครั้ง มันได้ข้อความที่ไม่เหมือนต้นฉบับ   คราวนี้เกิดจากข้อความที่ใส่ลงไปใน input มันไม่ใช่ unicode ที่ถูกต้องซะเอง แล้วเรื่องนี้มันเกิดขึ้นได้อย่างไร?

มา debug กันอีกทีดีกว่า

## แก้ข้อผิดพลาดเมื่อย้อนข้อความซ้ำ

ตอนนี้ เราจะมา debug ตอนที่ย้อนข้อความครั้งที่สอง และทำทำการแก้ไขให้มันทำงานถูกต้องกัน

ก่อนจะไปต่อ อยากให้คุณใช้เวลาอย่างเต็มที่ ในการคิดและแก้ไขข้อผิดพลาดนี้ด้วยตัวเองดูก่อน

### วิเคราะห์ปัญหา

เหมือนที่บอกไปแล้วว่ามันมีวิธีทำหลายวิธีในการ debug ซึ่ง [debugger](https://github.com/golang/vscode-go/blob/master/docs/debugging.md) เป็นวิธีที่สุดยอดมาก แต่เรายังคงจะใช้วิธี log เหมือนเดิม 😅

### เขียนโค้ด

1. ใน text editor ให้แทนที่ Reverse ตามนี้

```go
func Reverse(s string) string {
    fmt.Printf("input: %q\n", s)
    r := []rune(s)
    fmt.Printf("runes: %q\n", r)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}
```

นี่จะช่วยให้เราเข้าใจว่ามันเกิดความผิดพลาดขึ้นตรงไหนกันแน่

### รันโค้ด

ครั้งนี้เราจะเทสเฉพาะที่ทำงานผิดพลาดเพื่อวิเคราะห์ log ด้วยคำสั่ง go test -run

```sh
$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
        reverse_test.go:18: Before: "\x91", after: "�"
FAIL
exit status 1
FAIL    example/fuzz    0.145s
```

ุการเจาะจงไปที่ corpus ตรงๆใน FuzzXxx/testdata เราสามารถให้ `{FuzzTestName}/{filename}` ไปใน -run ได้ ซึ่งจะช่วยให้เรา debug ได้ง่ายขึ้น

ตอนนี้เรารู้แล้วว่า input มีหน้าตาที่ไม่ใช่ unicode ที่ถูกต้อง เรามาแก้ไขเรื่องนี้กัน

### แก้ปัญหา

เพื่อการแก้ปัญหาเรื่องนี้ เราจะคืน error ออกมาถ้าหากว่าเจอ input ที่ไม่ถูกต้องตามรูปแบบของ UTF-8

### เขียนโค้ด

1. ใน text editor ให้แทนที่ฟังก์ชัน Reverse ด้วยโค้ดตามนี้

```go
func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}
```

สิ่งนี้จะเปลี่ยนวิธีคืนค่าของฟังก์ชัน ซึ่งถ้าเจอว่า input ไม่ถูกต้องตาม UTF-8 ก็จะได้ error ออกมา

2. และเนื่องจากตอนนี้ฟังก์ชันมีการคืน error ออกมาด้วย เราก็ต้องแก้ไขฟังก์ชัน main เพื่อให้สอนคล้องกันตามนี้

```go
func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}
```

การเรียก Reverse ควรจะได้ error เป็นค่า nil ถ้าหากว่า input เป็น UTF-8 ที่ถูกต้อง

3. เราจะต้อง import errors และ unicode/utf8 เข้ามาด้วยตามนี้

```go
import (
    "errors"
    "fmt"
    "unicode/utf8"
)
```

4. แก้ไข reverse_test.go เพื่อตรวจสอบ error และ หากว่าเจอ error ก็ให้ข้ามเทสนั้นไปตามนี้

```go
func FuzzReverse(f *testing.F) {
    testcases := []string {"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
             return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}
```

นอกจากการ return เรายังสามารถใช้ t.Skip() แทนได้ ในกรณีที่ต้องการข้ามเทสของ input นั้น

### รันโค้ด

1. รันเทสด้วยคำสั่ง go test

```sh
$ go test
PASS
ok      example/fuzz  0.019s
```

2. รัน Fuzz ด้วยคำสั่ง go test -fuzz=Fuzz และหยุดมันเมื่อผ่านไปสักสองสามวินาที ด้วยการกด Ctrl-C

```sh
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
...
fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
PASS
ok      example/fuzz  228.000s
```

การทดสอบ fuzz จะทำงานไปเรื่อยๆจนกว่าจะเจอข้อผิดพลาด ถ้าไม่อยากรอไปเรื่อยๆ ให้เพิ่ม flag -fuzztime เข้าไป ไม่งั้นมันจะเทสไปตลอดกาล หรือก็คอยกด Ctrl-C เอาเอง

3. ทดสอบ Fuzz อีกสักที ด้วยคำสั่ง `go test -fuzz=Fuzz -fuzztime 30s` เพื่อให้มันทดสอบ fuzz ภายในเวลา 30 วินาที และให้มันจบการทดสอบหากไม่เจอข้อผิดพลาด

```sh
$ go test -fuzz=Fuzz -fuzztime 30s
fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
fuzz: elapsed: 6s, execs: 210803 (43501/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 9s, execs: 292882 (27360/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 12s, execs: 371872 (26329/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 15s, execs: 517169 (48433/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 18s, execs: 663276 (48699/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 21s, execs: 771698 (36143/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 24s, execs: 924768 (50990/sec), new interesting: 16 (total: 16)
fuzz: elapsed: 27s, execs: 1082025 (52427/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
PASS
ok      example/fuzz  31.025s
```

ทดสอบผ่าน!

เกี่ยวกับ flag ใหม่ `-fuzz` ยังมีอย่างอื่นเพิ่มเติมสำหรับ go test ไปดูได้ที่ [documentation](https://go.dev/doc/fuzz/#custom-settings)

## สรุป

สุดยอดเลย ตอนนี้เราก็ได้พาตัวเองไปรู้จักกับ fuzzing ใน Go เรียบร้อย

ขั้นตอนต่อไปคือ กลับไปมองหาฟังก์ชันในโค้ดของเรา ที่อยากเอามาลองทำ fuzz ดู แล้วทดลองเลย! แล้วถ้าหากว่่า fuzzing สามารถเจอบั๊กในโค้ดของคุณนะ สามารถไปเติมไว้ที่ [trophy case](https://github.com/golang/go/wiki/Fuzzing-trophy-case) ให้เราได้

ถ้าคุณพบปัญหาใดๆ หรือมีข้อเสนอแนะเกี่ยวกับฟีเจอร์ใหม่ๆ สามารถบอกเราได้ที่ [file and issue](https://github.com/golang/go/issues/new/?&labels=fuzz)

สามารถไปพูดคุยแลกเปลี่ยนกับเราในเรื่องต่างเกี่ยวกับฟีเจอร์ได้ใน Gpher Slack [#fuzzing channel](https://gophers.slack.com/archives/CH5KV1AKE)

เอกสารสำหรับอ่านเพิ่มเติมอยู่ที่ [go.dev/doc/fuzz](https://go.dev/doc/fuzz/#requirements)

โค้ดที่เสร็จสมบรูณ์

— main.go —

```go
package main

import (
    "errors"
    "fmt"
    "unicode/utf8"
)

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}
```

— reverse_test.go —

```go
package main

import (
    "testing"
    "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc) // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
            return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}
```