☰ Mục lục series
Tài liệu Go tiếng Việt
  1. Bắt đầu (Getting started)
    1. Tổng quan
    2. Tải về và cài đặt
    3. Bắt đầu với Go
    4. Quản lý các bản cài đặt Go
    5. Plugin trình soạn thảo và IDE
  2. Dạo quanh Go (A Tour of Go)
    1. Chào mừng
    2. Package, biến và hàm
    3. Điều khiển luồng
    4. Struct, slice và map
    5. Method và interface
    6. Generics
    7. Xử lý đồng thời
  3. Hướng dẫn (Tutorials)
    1. Danh sách hướng dẫn
    2. Tạo một Go module
    3. Gọi mã của bạn từ một module khác
    4. Trả về và xử lý lỗi
    5. Trả về lời chào ngẫu nhiên
    6. Trả về lời chào cho nhiều người
    7. Thêm một test
    8. Biên dịch và cài đặt ứng dụng
    9. Kết luận
    10. Bắt đầu với workspace nhiều module
    11. Phát triển RESTful API với Go và Gin
    12. Truy cập cơ sở dữ liệu quan hệ
    13. Bắt đầu với generics
    14. Bắt đầu với fuzzing
    15. Làm việc với JSON
    16. Tìm và sửa các dependency có lỗ hổng với govulncheck
    17. Tìm và khắc phục phụ thuộc có lỗ hổng với VS Code Go
    18. Viết ứng dụng web
  4. Viết code Go
    1. Cách viết mã Go
    2. Effective Go
    3. Doc Comment trong Go
  5. Module
    1. Phát triển và publish module
    2. Quản lý phụ thuộc
    3. Tổ chức một module Go
    4. Quản lý mã nguồn của module
    5. Cách đánh số phiên bản module
    6. Quy trình phát hành và đánh phiên bản module
    7. Publish một module
    8. Phát triển một bản cập nhật major version
    9. Tham chiếu tệp go.mod
    10. Ngừng hỗ trợ 'go get' cho việc cài đặt tệp thực thi
  6. Cơ sở dữ liệu (Database)
    1. Truy cập cơ sở dữ liệu quan hệ
    2. Mở một handle cơ sở dữ liệu
    3. Thực thi câu lệnh SQL không trả về dữ liệu
    4. Truy vấn dữ liệu
    5. Dùng prepared statement
    6. Thực thi transaction
    7. Huỷ các thao tác đang chạy
    8. Quản lý kết nối
    9. Tránh rủi ro SQL injection
  7. Chẩn đoán và hiệu năng
    1. Chẩn đoán
    2. Hướng dẫn về Go Garbage Collector
    3. Tối ưu hoá dựa trên profile
    4. Data Race Detector
    5. Gỡ lỗi mã Go bằng GDB
    6. Hỗ trợ coverage profiling cho integration test
  8. Bảo mật (Security)
    1. Tổng quan bảo mật
    2. Thực hành tốt về bảo mật cho lập trình viên Go
    3. Quản lý lỗ hổng trong Go
    4. Cơ sở dữ liệu lỗ hổng của Go
    5. Quét lỗ hổng trong IDE
    6. Chính sách CNA của Go
    7. Go Fuzzing
    8. Chi tiết kỹ thuật của Go Fuzzing
    9. Chính sách bảo mật của Go
    10. Threat Model của Go
    11. Các quyết định về bảo mật của Go
    12. Tuân thủ FIPS 140-3
  9. Công cụ và toolchain
    1. Tài liệu về các lệnh
    2. Về lệnh go
    3. Go Toolchains
    4. Telemetry của Go
    5. Cài đặt Go từ mã nguồn
    6. Cài đặt và sử dụng gccgo
  10. Tham khảo
    1. Câu hỏi thường gặp (FAQ)
    2. Go 1 và tương lai của các chương trình Go
    3. Hướng dẫn chuyển đổi sang encoding/json/v2
    4. Hướng dẫn đóng góp
    5. Đóng góp cho frontend gccgo
  11. Phụ lục
    1. Bảng thuật ngữ
Hướng dẫn (Tutorials)

Hướng dẫn: Bắt đầu với fuzzing

Bản dịch từ go.dev/doc/tutorial/fuzz

Hướng dẫn này giới thiệu những kiến thức cơ bản về fuzzing (kỹ thuật sinh dữ liệu đầu vào ngẫu nhiên để dò lỗi) trong Go. Với fuzzing, dữ liệu ngẫu nhiên được đưa vào chạy với test của bạn nhằm tìm ra các lỗ hổng hoặc các input gây crash. Một vài ví dụ về lỗ hổng mà fuzzing có thể tìm ra là SQL injection, buffer overflow, denial of service và cross-site scripting.

Trong hướng dẫn này, bạn sẽ viết một fuzz test cho một hàm đơn giản, chạy lệnh go, rồi debug và sửa các vấn đề trong mã nguồn.

Nếu cần trợ giúp về thuật ngữ xuyên suốt hướng dẫn này, xem bảng thuật ngữ Go Fuzzing.

Bạn sẽ đi qua các phần sau:

  1. Tạo một thư mục cho mã nguồn của bạn.
  2. Thêm mã nguồn để test.
  3. Thêm một unit test.
  4. Thêm một fuzz test.
  5. Sửa hai lỗi.
  6. Khám phá thêm tài nguyên khác.

Lưu ý: Với các hướng dẫn khác, xem Tutorials.

Lưu ý: Go fuzzing hiện hỗ trợ một tập con các kiểu dữ liệu dựng sẵn, được liệt kê trong tài liệu Go Fuzzing; sẽ có thêm nhiều kiểu dựng sẵn khác được hỗ trợ trong tương lai.

Điều kiện tiên quyết

  • Go. Chúng tôi khuyến nghị dùng phiên bản Go mới nhất để theo hướng dẫn này. Xem hướng dẫn cài đặt tại Installing Go.
  • Một công cụ để chỉnh sửa mã nguồn. Bất kỳ trình soạn thảo văn bản nào bạn có đều dùng được.
  • Một cửa sổ dòng lệnh. Go hoạt động tốt với mọi terminal trên Linux và Mac, và với PowerShell hoặc cmd trên Windows.
  • Một môi trường hỗ trợ fuzzing. Go fuzzing với coverage instrumentation hiện chỉ khả dụng trên kiến trúc AMD64 và ARM64.

Tạo một thư mục cho mã nguồn của bạn

Trước tiên, hãy tạo một thư mục cho mã nguồn mà bạn sẽ viết.

  1. Mở cửa sổ dòng lệnh và chuyển về thư mục home của bạn.

    Trên Linux hoặc Mac:

    $ cd

    Trên Windows:

    C:\> cd %HOMEPATH%

    Phần còn lại của hướng dẫn sẽ hiển thị dấu $ làm dấu nhắc lệnh. Các câu lệnh bạn dùng cũng sẽ chạy được trên Windows.

  2. Từ dấu nhắc lệnh, tạo một thư mục cho mã nguồn của bạn tên là fuzz.

    $ mkdir fuzz
    $ cd fuzz
  3. Tạo một module để chứa mã nguồn của bạn.

    Chạy câu lệnh go mod init, truyền cho nó module path của mã nguồn mới.

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

    Lưu ý: Với mã nguồn dùng trong môi trường thật, bạn sẽ chỉ định một module path phù hợp hơn với nhu cầu của mình. Để biết thêm, hãy xem Managing dependencies.

Tiếp theo, bạn sẽ thêm một đoạn mã đơn giản để đảo ngược một chuỗi, thứ mà chúng ta sẽ fuzz sau đó.

Thêm mã nguồn để test

Ở bước này, bạn sẽ thêm một hàm để đảo ngược một chuỗi.

Viết mã nguồn

  1. Dùng trình soạn thảo văn bản, tạo một tệp tên là main.go trong thư mục fuzz.

  2. Vào main.go, ở đầu tệp, dán khai báo package sau đây.

    package main

    Một chương trình độc lập (khác với thư viện) luôn nằm trong package main.

  3. Bên dưới khai báo package, dán khai báo hàm sau đây.

    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)
    }

    Hàm này sẽ nhận vào một string, lặp qua từng byte một, và trả về chuỗi đã được đảo ngược ở cuối.

    Lưu ý: Mã nguồn này dựa trên hàm stringutil.Reverse trong golang.org/x/example.

  4. Ở đầu main.go, bên dưới khai báo package, dán hàm main sau đây để khởi tạo một chuỗi, đảo ngược nó, in kết quả ra, và lặp lại.

    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)
    }

    Hàm này sẽ chạy vài thao tác Reverse, rồi in kết quả ra dòng lệnh. Điều này có thể hữu ích để thấy mã nguồn hoạt động thực tế, và có thể hữu ích cho việc debug.

  5. Hàm main dùng package fmt, nên bạn sẽ cần import nó.

    Những dòng mã đầu tiên sẽ trông như thế này:

    package main
    
    import "fmt"

Chạy mã nguồn

Từ dòng lệnh trong thư mục chứa main.go, hãy chạy mã nguồn.

$ 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"

Bạn có thể thấy chuỗi ban đầu, kết quả của việc đảo ngược nó, rồi kết quả của việc đảo ngược lần nữa, vốn tương đương với chuỗi ban đầu.

Giờ mã nguồn đã chạy được, đã đến lúc test nó.

Thêm một unit test

Ở bước này, bạn sẽ viết một unit test cơ bản cho hàm Reverse.

Viết mã nguồn

  1. Dùng trình soạn thảo văn bản, tạo một tệp tên là reverse_test.go trong thư mục fuzz.

  2. Dán đoạn mã sau vào 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)
            }
        }
    }

    Test đơn giản này sẽ khẳng định rằng các chuỗi input được liệt kê sẽ được đảo ngược đúng.

Chạy mã nguồn

Chạy unit test bằng go test

$ go test
PASS
ok      example/fuzz  0.013s

Tiếp theo, bạn sẽ đổi unit test này thành một fuzz test.

Thêm một fuzz test

Unit test có hạn chế của nó, cụ thể là mỗi input đều phải do lập trình viên thêm vào test. Một lợi ích của fuzzing là nó tự nghĩ ra input cho mã nguồn của bạn, và có thể phát hiện những trường hợp biên (edge case) mà các test case bạn nghĩ ra không chạm tới.

Trong phần này, bạn sẽ chuyển unit test thành một fuzz test để có thể sinh ra nhiều input hơn với ít công sức hơn!

Lưu ý rằng bạn có thể giữ unit test, benchmark và fuzz test trong cùng một tệp *_test.go, nhưng với ví dụ này bạn sẽ chuyển unit test thành fuzz test.

Viết mã nguồn

Trong trình soạn thảo văn bản, thay thế unit test trong reverse_test.go bằng fuzz test sau đây.

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 cũng có một vài hạn chế. Trong unit test, bạn có thể dự đoán được kết quả mong đợi của hàm Reverse, và kiểm chứng rằng kết quả thực tế khớp với mong đợi đó.

Ví dụ, trong test case Reverse("Hello, world"), unit test chỉ định giá trị trả về là "dlrow ,olleH".

Khi fuzzing, bạn không thể dự đoán kết quả mong đợi, vì bạn không kiểm soát được các input.

Tuy nhiên, có một vài tính chất của hàm Reverse mà bạn có thể kiểm chứng trong một fuzz test. Hai tính chất được kiểm tra trong fuzz test này là:

  1. Đảo ngược một chuỗi hai lần thì giữ nguyên giá trị ban đầu
  2. Chuỗi đã đảo ngược vẫn giữ trạng thái là UTF-8 hợp lệ.

Hãy để ý các khác biệt về cú pháp giữa unit test và fuzz test:

  • Hàm bắt đầu bằng FuzzXxx thay vì TestXxx, và nhận *testing.F thay vì *testing.T
  • Ở chỗ bạn thường thấy lời gọi t.Run, thì thay vào đó bạn thấy f.Fuzz, vốn nhận vào một fuzz target function mà các tham số là *testing.T và các kiểu dữ liệu cần fuzz. Các input từ unit test của bạn được cung cấp làm seed corpus input thông qua f.Add.

Hãy đảm bảo package mới, unicode/utf8, đã được import.

package main

import (
    "testing"
    "unicode/utf8"
)

Khi unit test đã được chuyển thành fuzz test, đã đến lúc chạy lại test.

Chạy mã nguồn

  1. Chạy fuzz test mà không fuzz nó, để chắc chắn rằng các seed input đều pass.

    $ go test
    PASS
    ok      example/fuzz  0.013s

    Bạn cũng có thể chạy go test -run=FuzzReverse nếu bạn có các test khác trong tệp đó và chỉ muốn chạy fuzz test.

  2. Chạy FuzzReverse có fuzzing, để xem liệu có chuỗi input sinh ngẫu nhiên nào gây lỗi hay không. Việc này được thực hiện bằng go test với một flag mới, -fuzz, đặt bằng tham số Fuzz. Hãy sao chép câu lệnh bên dưới.

    $ go test -fuzz=Fuzz

    Một flag hữu ích khác là -fuzztime, giới hạn thời gian fuzzing chạy. Ví dụ, chỉ định -fuzztime 10s ở test bên dưới sẽ có nghĩa là, miễn là không có lỗi nào xảy ra sớm hơn, thì test sẽ mặc định thoát sau khi đã trôi qua 10 giây. Xem phần này của tài liệu cmd/go để xem các flag testing khác.

    Giờ hãy chạy câu lệnh bạn vừa sao chép.

    $ 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

    Một lỗi đã xảy ra trong lúc fuzzing, và input gây ra vấn đề được ghi vào một tệp seed corpus; tệp này sẽ được chạy trong lần gọi go test tiếp theo, kể cả khi không có flag -fuzz. Để xem input đã gây lỗi, hãy mở tệp corpus được ghi vào thư mục testdata/fuzz/FuzzReverse bằng một trình soạn thảo văn bản. Tệp seed corpus của bạn có thể chứa một chuỗi khác, nhưng định dạng thì giống nhau.

    go test fuzz v1
    string("泃")

    Dòng đầu tiên của tệp corpus cho biết phiên bản mã hoá. Mỗi dòng tiếp theo biểu diễn giá trị của từng kiểu dữ liệu tạo nên mục corpus đó. Vì fuzz target chỉ nhận 1 input, nên chỉ có 1 giá trị sau dòng phiên bản.

  3. Chạy lại go test mà không có flag -fuzz; mục seed corpus gây lỗi mới sẽ được dùng:

    $ 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

    Vì test của chúng ta đã fail, đã đến lúc debug.

Sửa lỗi chuỗi không hợp lệ

Trong phần này, bạn sẽ debug lỗi này và sửa bug.

Bạn cứ thoải mái dành chút thời gian suy nghĩ và tự thử sửa vấn đề trước khi đi tiếp.

Chẩn đoán lỗi

Có một vài cách khác nhau để bạn debug lỗi này. Nếu bạn dùng VS Code làm trình soạn thảo văn bản, bạn có thể thiết lập trình debug để điều tra.

Trong hướng dẫn này, chúng tôi sẽ ghi log những thông tin debug hữu ích ra terminal của bạn.

Trước hết, hãy xem tài liệu của utf8.ValidString.

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

Hàm Reverse hiện tại đảo ngược chuỗi theo từng byte, và đó chính là vấn đề của chúng ta. Để giữ nguyên các rune được mã hoá UTF-8 của chuỗi ban đầu, thay vào đó chúng ta phải đảo ngược chuỗi theo từng rune.

Để xem xét vì sao input (trong trường hợp này là ký tự tiếng Trung 泃) lại khiến Reverse tạo ra một chuỗi không hợp lệ khi đảo ngược, bạn có thể kiểm tra số lượng rune trong chuỗi đã đảo ngược.

Viết mã nguồn

Trong trình soạn thảo văn bản, thay thế fuzz target bên trong FuzzReverse bằng đoạn sau.

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)
    }
})

Dòng t.Logf này sẽ in ra dòng lệnh nếu có lỗi xảy ra, hoặc nếu bạn chạy test với -v, điều này có thể giúp bạn debug chính vấn đề này.

Chạy mã nguồn

Chạy test bằng 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

Toàn bộ seed corpus dùng các chuỗi mà trong đó mỗi ký tự đều chỉ chiếm một byte. Tuy nhiên, những ký tự như 泃 có thể cần nhiều byte. Vì vậy, đảo ngược chuỗi theo từng byte sẽ làm hỏng các ký tự nhiều byte.

Lưu ý: Nếu bạn tò mò về cách Go xử lý chuỗi, hãy đọc bài blog Strings, bytes, runes and characters in Go để hiểu sâu hơn.

Với hiểu biết tốt hơn về bug này, hãy sửa lỗi trong hàm Reverse.

Sửa lỗi

Để sửa hàm Reverse, hãy duyệt chuỗi theo từng rune, thay vì theo từng byte.

Viết mã nguồn

Trong trình soạn thảo văn bản, thay thế hàm Reverse() hiện có bằng đoạn sau.

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)
}

Khác biệt mấu chốt là giờ Reverse lặp qua từng rune trong chuỗi, thay vì từng byte. Lưu ý rằng đây chỉ là một ví dụ, và nó không xử lý đúng ký tự tổ hợp.

Chạy mã nguồn

  1. Chạy test bằng go test

    $ go test
    PASS
    ok      example/fuzz  0.016s

    Giờ test đã pass!

  2. Fuzz lại lần nữa bằng go test -fuzz, để xem có bug mới nào không.

    $ 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

    Chúng ta thấy rằng chuỗi khác với chuỗi ban đầu sau khi bị đảo ngược hai lần. Lần này chính input là unicode không hợp lệ. Sao điều đó lại có thể xảy ra nếu chúng ta đang fuzz với các chuỗi?

    Hãy debug lại lần nữa.

Sửa lỗi đảo ngược hai lần

Trong phần này, bạn sẽ debug lỗi đảo ngược hai lần và sửa bug.

Bạn cứ thoải mái dành chút thời gian suy nghĩ và tự thử sửa vấn đề trước khi đi tiếp.

Chẩn đoán lỗi

Như lần trước, có vài cách để bạn debug lỗi này. Trong trường hợp này, dùng trình debug sẽ là một cách rất tốt.

Trong hướng dẫn này, chúng tôi sẽ ghi log những thông tin debug hữu ích trong hàm Reverse.

Hãy nhìn kỹ chuỗi đã đảo ngược để phát hiện lỗi. Trong Go, một chuỗi là một slice byte chỉ đọc, và có thể chứa những byte không phải UTF-8 hợp lệ. Chuỗi ban đầu là một byte slice với một byte, '\x91'. Khi chuỗi input được chuyển sang []rune, Go mã hoá byte slice đó sang UTF-8, và thay thế byte kia bằng ký tự UTF-8 �. Khi chúng ta so sánh ký tự UTF-8 thay thế đó với byte slice input, rõ ràng chúng không bằng nhau.

Viết mã nguồn

  1. Trong trình soạn thảo văn bản, thay thế hàm Reverse bằng đoạn sau.

    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)
    }

    Điều này sẽ giúp chúng ta hiểu có gì sai khi chuyển chuỗi thành một slice các rune.

Chạy mã nguồn

Lần này, chúng ta chỉ muốn chạy test đang fail để xem log. Để làm việc đó, chúng ta sẽ dùng go test -run.

Để chạy một mục corpus cụ thể trong FuzzXxx/testdata, bạn có thể truyền {FuzzTestName}/{filename} cho -run. Điều này có thể hữu ích khi debug. Trong trường hợp này, đặt flag -run bằng đúng mã hash của test đang fail. Sao chép và dán mã hash duy nhất từ terminal của bạn; nó sẽ khác với mã bên dưới.

$ 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

Biết rằng input là unicode không hợp lệ, hãy sửa lỗi trong hàm Reverse của chúng ta.

Sửa lỗi

Để khắc phục vấn đề này, hãy trả về một lỗi nếu input của Reverse không phải UTF-8 hợp lệ.

Viết mã nguồn

  1. Trong trình soạn thảo văn bản, thay thế hàm Reverse hiện có bằng đoạn sau.

    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
    }

    Thay đổi này sẽ trả về một lỗi nếu chuỗi input chứa các ký tự không phải UTF-8 hợp lệ.

  2. Vì hàm Reverse giờ trả về một lỗi, hãy sửa hàm main để bỏ qua giá trị lỗi thêm vào đó. Thay thế hàm main hiện có bằng đoạn sau.

    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)
    }

    Những lời gọi Reverse này sẽ trả về lỗi nil, vì chuỗi input là UTF-8 hợp lệ.

  3. Bạn sẽ cần import các package errors và unicode/utf8. Câu lệnh import trong main.go sẽ trông như sau.

    import (
        "errors"
        "fmt"
        "unicode/utf8"
    )
  4. Sửa tệp reverse_test.go để kiểm tra lỗi và bỏ qua test bằng cách return nếu có lỗi được sinh ra.

    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)
            }
        })
    }

    Thay vì return, bạn cũng có thể gọi t.Skip() để dừng việc thực thi với fuzz input đó.

Chạy mã nguồn

  1. Chạy test bằng go test

    $ go test
    PASS
    ok      example/fuzz  0.019s
  2. Fuzz nó bằng go test -fuzz=Fuzz, rồi sau khi vài giây trôi qua, dừng fuzzing bằng ctrl-C. Fuzz test sẽ chạy cho tới khi gặp một input gây lỗi, trừ khi bạn truyền flag -fuzztime. Mặc định là chạy mãi mãi nếu không có lỗi nào xảy ra, và tiến trình có thể bị ngắt bằng 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
  1. Fuzz nó bằng go test -fuzz=Fuzz -fuzztime 30s, lệnh này sẽ fuzz trong 30 giây rồi thoát nếu không tìm thấy lỗi nào.

    $ 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

    Fuzzing đã pass!

    Ngoài flag -fuzz, một số flag mới đã được thêm vào go test và có thể xem trong tài liệu.

    Xem Go Fuzzing để biết thêm thông tin về các thuật ngữ dùng trong output của fuzzing. Ví dụ, “new interesting” chỉ những input mở rộng được code coverage của corpus thuộc fuzz test hiện có. Số lượng input “new interesting” thường tăng mạnh khi fuzzing mới bắt đầu, vọt lên vài lần khi các nhánh mã mới được phát hiện, rồi giảm dần theo thời gian.

Kết luận

Làm tốt lắm! Bạn vừa mới làm quen với fuzzing trong Go.

Bước tiếp theo là chọn một hàm trong mã nguồn của bạn mà bạn muốn fuzz, và thử xem sao! Nếu fuzzing tìm ra một bug trong mã nguồn của bạn, hãy cân nhắc thêm nó vào trophy case.

Nếu bạn gặp vấn đề nào đó hoặc có ý tưởng về một tính năng, hãy tạo một issue.

Để thảo luận và góp ý chung về tính năng này, bạn cũng có thể tham gia kênh #fuzzing trong Gophers Slack.

Hãy xem tài liệu tại go.dev/security/fuzz để đọc thêm.

Mã nguồn hoàn chỉnh

-– 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
}

-– 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)
        }
    })
}

Lên đầu trang

Nội dung gốc: Go Documentation © The Go Authors, giấy phép CC BY 3.0. Bản dịch và ghi chú: Lê Văn Dũng.