trailofbits/go-panikint

GitHub: trailofbits/go-panikint

一个修改版 Go 编译器,在编译期注入运行时检测,自动捕获整数溢出、下溢和类型截断问题并触发 panic。

Stars: 42 | Forks: 3

# 注意 **⚠️ go-panikint 正在被整合进 [gosentry](https://github.com/kevin-valerio/gosentry),这是一个专注于安全的 Go 工具集。它集成了 LibAFL fuzzing、go-panikint 等功能。如果你打算使用 go-panikint,应该先看看 [gosentry](https://github.com/kevin-valerio/gosentry)。** ## Go-Panikint [![Go-panikint self-compile and test](https://static.pigsec.cn/wp-content/uploads/repos/2026/03/8e8d5ae7d2220557.svg)](https://github.com/trailofbits/go-panikint/actions/workflows/go.yml) ### 概述 `Go-Panikint` 是 Go 编译器的一个修改版本,它为整数算术运算添加了**自动溢出/下溢检测**,并为整数转换添加了**类型截断检测**。当检测到溢出或截断时,会触发一个包含详细错误信息的 **panic**,其中包括具体的操作类型和涉及的整数类型。 **算术运算**:处理有符号和无符号整数类型的加法 `+`、减法 `-`、乘法 `*` 和除法 `/`。对于有符号整数,涵盖 `int8`、`int16`、`int32`。对于无符号整数,涵盖 `uint8`、`uint16`、`uint32`、`uint64`。除法情况专门检测有符号整数的 `MIN_INT / -1` 溢出条件。`int64` 和 `uintptr` 不进行算术运算检查。 **类型截断检测**:检测当目标类型的范围小于源类型时,整数类型转换是否会导致数据丢失。涵盖所有整数类型:`int8`、`int16`、`int32`、`int64`、`uint8`、`uint16`、`uint32`、`uint64`。由于平台相关的用法,排除 `uintptr`。默认**禁用**。 ### 使用和安装: ``` # 克隆、更改目录并编译编译器 git clone https://github.com/trailofbits/go-panikint && cd go-panikint/src # 启用所有功能编译 ./make.bash # 分叉编译器根目录的完整路径 export GOROOT=/path/to/go-panikint # 编译并运行 Go 程序 ./bin/go run test_simple_overflow.go ###### # 或者:构建启用截断检测的编译器 GOFLAGS="-gcflags=-truncationdetect=true" ./make.bash # 仅 Fuzz ./bin/go test -fuzz=FuzzIntegerOverflow -v ``` ### 它是如何工作的? #### 具体做了什么? 我们基本上修改了 Go 编译器的中间表示 (IR) 部分,以便在每个数学操作数(即 `OADD`、`OMUL`、`OSUB`、`ODIV` 等)和类型转换上,编译器不仅添加执行操作的 IR 操作码,而且还**插入**一堆算术错误检查并插入带有详细错误信息的 panic 调用。Panic 消息包括具体的操作类型(例如,“integer overflow in int8 addition operation”、“integer truncation: uint16 cannot fit in uint8”)。这段代码最终会变成应用程序的二进制代码(汇编),因此请谨慎使用。 下面是一个 Ghidra 反编译加法 `+` 的例子: ``` if (*x_00 == '+') { val = (uint32)*(undefined8 *)(puVar9 + 0x60); sVar23 = val + sVar21; puVar17 = puVar9 + 8; if (((sdword)val < 0 && sVar21 < 0) && (sdword)val < sVar23 || ((sdword)val >= 0 && sVar21 >= 0) && sVar23 < (sdword)val) { runtime.panicoverflow(); // <-- panic if overflow caught } goto LAB_1000a10d4; } ``` #### 为什么我们使用基于源代码位置的过滤? 正如在 `src/cmd/compile/internal/ssagen/ssa.go` 中实现的那样,我们对溢出检测应用了基于源代码位置的过滤。这确保了溢出检测仅应用于用户代码和目标应用程序(例如外部代码库的安全审计),同时排除标准库和第三方依赖。 每个算术运算(`intAdd`、`intSub`、`intMul`、`intDiv`)都使用 `n.Pos()` 和 `base.Ctxt.PosTable.Pos(pos).Filename()` 检查实际的源文件位置。来自包含 `/go-panikint/src/`、`/pkg/mod/`、`/vendor/` 的文件的操作会被自动排除,并且标准库包(`runtime`、`sync`、`os`、`syscall` 等)/ 内部包(`internal/*`)在编译器构建期间被排除。 ### 抑制假阳性 在操作所在的同一行或紧邻的上一行添加注释标记,以将 bug 标记为假阳性,这样编译器就不会在算术或截断问题上发生 panic: - 溢出/下溢:`overflow_false_positive` - 截断:`truncation_false_positive` 示例: ``` // This is an overflow, but it's on purpose so we don't care flagging it // overflow_false_positive intentional_overflow := a + b // Same for my buggy truncation // truncation_false_positive x := uint8(big) // Also work on the same line sum2 := a + b // overflow_false_positive x2 := uint8(big) // truncation_false_positive ``` 有时这可能不起作用,那是因为 Go 正在内联该函数。如果 `// overflow_false_poistive` 不起作用,请在函数签名前添加 `//go:noinline`。 ### 测试 你可以使用以下命令运行 `tests/` 中的测试套件: ``` cd tests/; GOROOT=/path/to/go-panikint /path/to/go-panikint/bin/go test -v . ``` CI 说明: - Pull request 和推送会运行 workflow linting(actionlint + zizmor)、`go vet` 以及使用构建的工具链在 `tests/` 中进行类型检查(`go test -run=^$`)。 - Pull request 和推送会运行构建 + 测试(包括禁用/启用截断两种情况)。 - 编译器可能会生成许多相同的 panic 消息字符串;字符串符号发出是串行化的,以避免罕见的 `go:string."..." redeclared` 构建失败。 注意:`$GOROOT/test` 中的上游 Go 发行版测试有意依赖于整数回绕。 go-panikint 单元测试位于 `$GOROOT/tests`(复数)中,应在启用溢出检查的情况下运行。 当运行 `cmd/internal/testdir` 时(例如通过 `src/all.bash`),我们使用 `GOPANIKINT_DISABLE_OVERFLOW=1` 禁用溢出检测。 ### Pre-commit (gofmt) 如果你使用 `pre-commit` 或 `prek`,此仓库包含一个最小的 `.pre-commit-config.yaml`,它会对暂存的 Go 文件运行 `gofmt`。该钩子跳过 `test/` 和任何 `testdata/` 路径,因为这些固件在 Go 仓库中故意不进行 gofmt 格式化。 示例: ``` prek run --all-files ``` ### 示例 #### 示例 1(有符号整数溢出): ``` package main import "fmt" func main() { fmt.Println("Testing signed integer overflow detection...") // Test int8 addition overflow var a int8 = 127 var b int8 = 1 fmt.Printf("Before: a=%d, b=%d\n", a, b) result := a + b // Should panic with "integer overflow in int8 addition operation" fmt.Printf("After: result=%d\n", result) } ``` #### 示例 2(无符号整数溢出): ``` package main import "fmt" func main() { fmt.Println("Testing unsigned integer overflow detection...") // Test uint8 addition overflow var a uint8 = 255 var b uint8 = 1 fmt.Printf("Before: a=%d, b=%d\n", a, b) result := a + b // Should panic with "integer overflow in uint8 addition operation" fmt.Printf("After: result=%d\n", result) } ``` **预期输出(对于有符号和无符号溢出):** ``` bash-5.2$ GOROOT=/path/to/go-panikint && ./bin/go run test_overflow.go Testing overflow detection... Before: a=127, b=1 panic: runtime error: integer overflow in int8 addition operation goroutine 1 [running]: main.main() /path/to/go-panikint/test_overflow.go:12 +0xfc exit status 2 ``` #### 示例 3(类型截断): ``` package main import "fmt" func main() { fmt.Println("Testing type truncation detection...") var u16 uint16 = 256 fmt.Printf("Before: u16=%d\n", u16) result := uint8(u16) // Should panic with "integer truncation: uint16 cannot fit in uint8" fmt.Printf("After: result=%d\n", result) } ``` **预期输出:** ``` bash-5.2$ GOROOT=/path/to/go-panikint && ./bin/go run test_truncation.go Testing type truncation detection... Before: u16=256 panic: runtime error: integer truncation: uint16 cannot fit in uint8 goroutine 1 [running]: main.main() /path/to/go-panikint/test_truncation.go:10 +0xfc exit status 2 ``` #### 示例 4(fuzzing): **Fuzzing harness:** ``` package fuzztest import "testing" func FuzzIntegerOverflow(f *testing.F) { f.Fuzz(func(t *testing.T, a, b int8) { result := a + b t.Logf("%d + %d = %d", a, b, result) }) } ``` **输出:** ``` GOROOT=/path/to/go-panikint/go-panikint ../bin/go test -fuzz=FuzzIntegerOverflow -v === RUN FuzzIntegerOverflow fuzz: elapsed: 0s, gathering baseline coverage: 0/15 completed fuzz: elapsed: 0s, gathering baseline coverage: 9/15 completed --- FAIL: FuzzIntegerOverflow (0.03s) --- FAIL: FuzzIntegerOverflow (0.00s) testing.go:1822: panic: runtime error: integer overflow in int8 addition operation goroutine 23 [running]: runtime/debug.Stack() /path/to/go-panikintgo-panikint/src/runtime/debug/stack.go:26 +0xc4 testing.tRunner.func1() /path/to/go-panikintgo-panikint/src/testing/testing.go:1822 +0x220 panic({0x100e27b00?, 0x100fa4d60?}) /path/to/go-panikintgo-panikint/src/runtime/panic.go:783 +0x120 fuzztest.FuzzIntegerOverflow.func1(0x0?, 0x0?, 0x0?) /path/to/go-panikintgo-panikint/fuzz_test/fuzz_test.go:10 +0xf8 reflect.Value.call({0x100e24ca0?, 0x100e5d5f8?, 0x14000093e28?}, {0x100dba042, 0x4}, {0x1400012a180, 0x3, 0x0?}) /path/to/go-panikintgo-panikint/src/reflect/value.go:581 +0x960 reflect.Value.Call({0x100e24ca0?, 0x100e5d5f8?, 0x14000154000?}, {0x1400012a180?, 0x100e5ce40?, 0x100d2abf3?}) /path/to/go-panikintgo-panikint/src/reflect/value.go:365 +0x94 testing.(*F).Fuzz.func1.1(0x140001028c0?) /path/to/go-panikintgo-panikint/src/testing/fuzz.go:341 +0x258 testing.tRunner(0x140001028c0, 0x14000152000) /path/to/go-panikintgo-panikint/src/testing/testing.go:1931 +0xc8 created by testing.(*F).Fuzz.func1 in goroutine 7 /path/to/go-panikintgo-panikint/src/testing/fuzz.go:328 +0x4a4 Failing input written to testdata/fuzz/FuzzIntegerOverflow/8a8466cc4de923f0 To re-run: go test -run=FuzzIntegerOverflow/8a8466cc4de923f0 ```
标签:DNS 反向解析, EVTX分析, EVTX分析, EVTX分析, Go编译器, Go语言, 云安全监控, 内存安全, 整数溢出检测, 日志审计, 程序插桩, 程序破解, 类型截断, 编译器修改, 运行时检查, 防御性编程, 静态分析