DEV Community

Pallat Anchaleechamaikorn
Pallat Anchaleechamaikorn

Posted on

เรียนรู้เรื่อง fuzzing ใน Go

#go

ต้นทางมาจาก Tutorial ของ Go อีกครั้งนะครับเกี่ยวกับเรื่อง fuzzing นี้

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

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

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

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

ในกรณีที่พบคำศัพท์ที่ไม่คุ้น ให้ไปดูที่เรื่อง Go Fuzzing glossary

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

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

Note: ในเวอร์ชั่นปัจจุบันจะรองรับเฉพาะ built-in types ตามเอกสารนี้ Go Fuzzing docs เท่านั้น ส่วนที่นอกเหนือจากนี้จะถูกเพิ่มในอนาคต

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

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

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

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

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

บน Linux หรือ Mac:

$ cd
Enter fullscreen mode Exit fullscreen mode

บน Windows:

C:\> cd %HOMEPATH%
Enter fullscreen mode Exit fullscreen mode

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

  1. จาก command prompt ให้สร้างไดเร็กทอรี่ชื่อ fuzz
$ mkdir fuzz
$ cd fuzz
Enter fullscreen mode Exit fullscreen mode
  1. สร้าง module ให้โค้ดของเราก่อน

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

$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz
Enter fullscreen mode Exit fullscreen mode

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

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

เขียนโค้ด

  1. ใช้ text editor สร้างไฟล์ชื่อ main.go ในไดเร็คทอรี่ fuzz
  2. ในไฟล์ main.go ให้ประกาศ package ไว้บนสุดตามนี้
package main
Enter fullscreen mode Exit fullscreen mode
  1. ต่อจากนั้นให้วางฟังก์ชันนี้ตามลงไป
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)
}
Enter fullscreen mode Exit fullscreen mode

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

  1. ต่อมาให้เพิ่มการเรียกใช้ฟังก์ชันลงไปในฟังก์ชัน main ตามนี้ โดยเริ่มจากพิมพ์ประโยคตั้งต้น แล้วย้อนมันครั้งหนึ่ง พิมพ์ออกมา แล้วก็ย้อนอีกรอบ
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)
}
Enter fullscreen mode Exit fullscreen mode

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

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

import "fmt"
Enter fullscreen mode Exit fullscreen mode

รันโค้ด

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

$ 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"
Enter fullscreen mode Exit fullscreen mode

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

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

เพิ่ม unit test

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

เขียนโค้ด

  1. ใช้ text editor สร้างไฟล์ชื่อ reverse_test.go ในไดเร็คทอรี่ fuzz
  2. วางโค้ดนี้ลงไปใน reverse_test.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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

รันโค้ด

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

$ go test
PASS
ok      example/fuzz  0.013s
Enter fullscreen mode Exit fullscreen mode

เรียบร้อย ขั้นตอนต่อไปเราจะไปเทสด้วย 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 ด้วยโค้ดชุดใหม่ตามนี้

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)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

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 เข้ามาแล้ว

package main

import (
    "testing"
    "unicode/utf8"
)
Enter fullscreen mode Exit fullscreen mode

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

รันโค้ด

  1. ทดลองรัน fuzz test โดยไม่ทำ fuzzing เพื่อให้มันใจว่า seed ของเราทำงานได้ก่อนสักที
$ go test
PASS
ok      example/fuzz  0.013s
Enter fullscreen mode Exit fullscreen mode

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

  1. รัน FuzzReverse ด้วย fuzzing เพื่อตรวจสอบการสร้าง input แบบสุ่มเข้าไปดูบ้างว่าจะมีอะไรผิดพลาดเกิดขึ้น ซึ่งคราวนี้เราจะใช้ flag ใหม่ -fuzz ร่วมด้วย
$ 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
Enter fullscreen mode Exit fullscreen mode

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

go test fuzz v1
string("泃")
Enter fullscreen mode Exit fullscreen mode

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

  1. ลองรัน go test อีกรอบโดยไม่ใส่ -fuz ดูจะพบว่า seed corpus จะถูกนำมาใช้ด้วย
$ 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
Enter fullscreen mode Exit fullscreen mode

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

ทำการแก้ไข error

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

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

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

ที่จริงก็มีหลายวิธีในการทำ debug ซึ่งถ้าเราใช้ VS Code อยู่แล้ว เราสามารถติดตั้งตัว debugger เพื่อใช้วิเคราะห์ปัญหากัน แต่ในบทเรียนนี้ เราจะใช้ 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 ด้วยโค้ดชุดนี้

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)
    }
})
Enter fullscreen mode Exit fullscreen mode

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

รันโค้ด

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

$ 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
Enter fullscreen mode Exit fullscreen mode

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

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

แก้ Error

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

เขียนโค้ด

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

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)
}
Enter fullscreen mode Exit fullscreen mode

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

รันโค้ด

  1. รันเทสด้วยคำสั่ง go test
$ go test
PASS
ok      example/fuzz  0.016s
Enter fullscreen mode Exit fullscreen mode

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

  1. เทส Fuzz อีกทีด้วยคำสั่ง go test -fuzz เพื่อตรวจหาบั๊กใหม่
$ 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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

เหมือนที่บอกไปแล้วว่ามันมีวิธีทำหลายวิธีในการ debug ซึ่ง debugger เป็นวิธีที่สุดยอดมาก แต่เรายังคงจะใช้วิธี log เหมือนเดิม 😅

เขียนโค้ด

  1. ใน text editor ให้แทนที่ Reverse ตามนี้
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)
}
Enter fullscreen mode Exit fullscreen mode

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

รันโค้ด

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

$ 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
Enter fullscreen mode Exit fullscreen mode

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

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

แก้ปัญหา

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

เขียนโค้ด

  1. ใน text editor ให้แทนที่ฟังก์ชัน Reverse ด้วยโค้ดตามนี้
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
}
Enter fullscreen mode Exit fullscreen mode

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

  1. และเนื่องจากตอนนี้ฟังก์ชันมีการคืน error ออกมาด้วย เราก็ต้องแก้ไขฟังก์ชัน main เพื่อให้สอนคล้องกันตามนี้
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)
}
Enter fullscreen mode Exit fullscreen mode

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

  1. เราจะต้อง import errors และ unicode/utf8 เข้ามาด้วยตามนี้
import (
    "errors"
    "fmt"
    "unicode/utf8"
)
Enter fullscreen mode Exit fullscreen mode
  1. แก้ไข reverse_test.go เพื่อตรวจสอบ error และ หากว่าเจอ error ก็ให้ข้ามเทสนั้นไปตามนี้
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)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

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

รันโค้ด

  1. รันเทสด้วยคำสั่ง go test
$ go test
PASS
ok      example/fuzz  0.019s
Enter fullscreen mode Exit fullscreen mode
  1. รัน Fuzz ด้วยคำสั่ง go test -fuzz=Fuzz และหยุดมันเมื่อผ่านไปสักสองสามวินาที ด้วยการกด Ctrl-C
$ 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
Enter fullscreen mode Exit fullscreen mode

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

  1. ทดสอบ Fuzz อีกสักที ด้วยคำสั่ง go test -fuzz=Fuzz -fuzztime 30s เพื่อให้มันทดสอบ fuzz ภายในเวลา 30 วินาที และให้มันจบการทดสอบหากไม่เจอข้อผิดพลาด
$ 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
Enter fullscreen mode Exit fullscreen mode

ทดสอบผ่าน!

เกี่ยวกับ flag ใหม่ -fuzz ยังมีอย่างอื่นเพิ่มเติมสำหรับ go test ไปดูได้ที่ documentation

สรุป

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

ขั้นตอนต่อไปคือ กลับไปมองหาฟังก์ชันในโค้ดของเรา ที่อยากเอามาลองทำ fuzz ดู แล้วทดลองเลย! แล้วถ้าหากว่่า fuzzing สามารถเจอบั๊กในโค้ดของคุณนะ สามารถไปเติมไว้ที่ trophy case ให้เราได้

ถ้าคุณพบปัญหาใดๆ หรือมีข้อเสนอแนะเกี่ยวกับฟีเจอร์ใหม่ๆ สามารถบอกเราได้ที่ file and issue

สามารถไปพูดคุยแลกเปลี่ยนกับเราในเรื่องต่างเกี่ยวกับฟีเจอร์ได้ใน Gpher Slack #fuzzing channel

เอกสารสำหรับอ่านเพิ่มเติมอยู่ที่ go.dev/doc/fuzz

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

— main.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
}
Enter fullscreen mode Exit fullscreen mode

— reverse_test.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)
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

Discussion (2)

Collapse
rutchaphon123 profile image
r_kidkarn

ไม่นึกว่าจะเจอคนไทย พอเขียนสอนใช้ api ด้วย go ไหมครับ

Collapse
pallat profile image
Pallat Anchaleechamaikorn Author

ผมมีที่สอนไว้ที่ skooldio ครับ