🧰
Goに入門するので何をするとクラッシュをするか実験をしたい
はじめに
Goを書く機会が出てきたので入門します。
前回はRustにチャレンジしており、興味があれば参照ください。
マシンスペック
- MacBook Air M2 arm64
- DockerでLinux/arm64の公式Goイメージを使用
準備
Docker の起動
docker run -it --rm -v "$(pwd)":/mnt golang:latest bash
開発ツール
apt-get update && apt-get install -y make vim build-essential
cd /mnt
go version
# go version go1.25.3 linux/arm64
検証項目(C, Rust, Go)
下記4観点でCとGo、Rustを並べて確認します。
| 項目 | C言語 | Rust | Go |
|---|---|---|---|
| 解放済みのメモリへのアクセス | |||
| Nullポインタ参照 | |||
| 不正な参照 | |||
| バッファオーバーラン |
検証
解放済みのメモリへのアクセス
解放済みのメモリにアクセスすると、ゴミ値が出てきたりNullを参照してしまうことになります。
Go
//go:build linux
package main
import (
"fmt"
"syscall"
"unsafe"
)
func main() {
const size = 4096
b, _ := syscall.Mmap(-1, 0, size,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS)
fmt.Println("mapped:", len(b), "bytes")
b[0] = 0xAA
p := unsafe.Pointer(&b[0])
_ = syscall.Munmap(b)
fmt.Println("unmapped; write after free...")
*(*byte)(p) = 0xBB
fmt.Println("should not reach here")
}
コンパイルは通ったので、実行結果。
mapped: 4096 bytes
unmapped; write after free...
unexpected fault address 0xffffbde28000
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x1 addr=0xffffbde28000 pc=0xa40ac]
goroutine 1 gp=0x40000021c0 m=0 mp=0x18b540 [running]:
runtime.throw({0xd37db?, 0x0?})
/usr/local/go/src/runtime/panic.go:1094 +0x34 fp=0x4000076e20 sp=0x4000076df0 pc=0x7c444
runtime.sigpanic()
/usr/local/go/src/runtime/signal_unix.go:939 +0x214 fp=0x4000076e80 sp=0x4000076e20 pc=0x7d914
main.main()
/mnt/uaf_mmap_linux.go:24 +0x11c fp=0x4000076f40 sp=0x4000076e90 pc=0xa40ac
runtime.main()
/usr/local/go/src/runtime/proc.go:285 +0x278 fp=0x4000076fd0 sp=0x4000076f40 pc=0x4cff8
runtime.goexit({})
/usr/local/go/src/runtime/asm_arm64.s:1268 +0x4 fp=0x4000076fd0 sp=0x4000076fd0 pc=0x82764
goroutine 2 gp=0x4000002700 m=nil [force gc (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:460 +0xc0 fp=0x4000040f90 sp=0x4000040f70 pc=0x7c510
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:466
runtime.forcegchelper()
/usr/local/go/src/runtime/proc.go:373 +0xb4 fp=0x4000040fd0 sp=0x4000040f90 pc=0x4d344
runtime.goexit({})
/usr/local/go/src/runtime/asm_arm64.s:1268 +0x4 fp=0x4000040fd0 sp=0x4000040fd0 pc=0x82764
created by runtime.init.7 in goroutine 1
/usr/local/go/src/runtime/proc.go:361 +0x24
goroutine 3 gp=0x4000002c40 m=nil [GC sweep wait]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:460 +0xc0 fp=0x4000041760 sp=0x4000041740 pc=0x7c510
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:466
runtime.bgsweep(0x4000062000)
/usr/local/go/src/runtime/mgcsweep.go:279 +0x9c fp=0x40000417b0 sp=0x4000041760 pc=0x38cdc
runtime.gcenable.gowrap1()
/usr/local/go/src/runtime/mgc.go:212 +0x28 fp=0x40000417d0 sp=0x40000417b0 pc=0x2cc58
runtime.goexit({})
/usr/local/go/src/runtime/asm_arm64.s:1268 +0x4 fp=0x40000417d0 sp=0x40000417d0 pc=0x82764
created by runtime.gcenable in goroutine 1
/usr/local/go/src/runtime/mgc.go:212 +0x6c
goroutine 4 gp=0x4000002e00 m=nil [GC scavenge wait]:
runtime.gopark(0x4000062000?, 0xfb170?, 0x1?, 0x0?, 0x4000002e00?)
/usr/local/go/src/runtime/proc.go:460 +0xc0 fp=0x4000041f60 sp=0x4000041f40 pc=0x7c510
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:466
runtime.(*scavengerState).park(0x18ab40)
/usr/local/go/src/runtime/mgcscavenge.go:425 +0x5c fp=0x4000041f90 sp=0x4000041f60 pc=0x3689c
runtime.bgscavenge(0x4000062000)
/usr/local/go/src/runtime/mgcscavenge.go:653 +0x44 fp=0x4000041fb0 sp=0x4000041f90 pc=0x36db4
runtime.gcenable.gowrap2()
/usr/local/go/src/runtime/mgc.go:213 +0x28 fp=0x4000041fd0 sp=0x4000041fb0 pc=0x2cbf8
runtime.goexit({})
/usr/local/go/src/runtime/asm_arm64.s:1268 +0x4 fp=0x4000041fd0 sp=0x4000041fd0 pc=0x82764
created by runtime.gcenable in goroutine 1
/usr/local/go/src/runtime/mgc.go:213 +0xac
goroutine 5 gp=0x4000003340 m=nil [GOMAXPROCS updater (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:460 +0xc0 fp=0x4000042770 sp=0x4000042750 pc=0x7c510
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:466
runtime.updateMaxProcsGoroutine()
/usr/local/go/src/runtime/proc.go:6720 +0xf4 fp=0x40000427d0 sp=0x4000042770 pc=0x5bcd4
runtime.goexit({})
/usr/local/go/src/runtime/asm_arm64.s:1268 +0x4 fp=0x40000427d0 sp=0x40000427d0 pc=0x82764
created by runtime.defaultGOMAXPROCSUpdateEnable in goroutine 1
/usr/local/go/src/runtime/proc.go:6708 +0x48
goroutine 6 gp=0x4000003500 m=nil [finalizer wait]:
runtime.gopark(0x0?, 0x0?, 0xb8?, 0x5?, 0x7cf64?)
/usr/local/go/src/runtime/proc.go:460 +0xc0 fp=0x4000040580 sp=0x4000040560 pc=0x7c510
runtime.runFinalizers()
/usr/local/go/src/runtime/mfinal.go:210 +0x104 fp=0x40000407d0 sp=0x4000040580 pc=0x2bcf4
runtime.goexit({})
/usr/local/go/src/runtime/asm_arm64.s:1268 +0x4 fp=0x40000407d0 sp=0x40000407d0 pc=0x82764
created by runtime.createfing in goroutine 1
/usr/local/go/src/runtime/mfinal.go:172 +0x78
fatalエラーで終了しました。
Nullポインタ参照
次はNullポインタの参照です。
Go
package main
func main() {
var p *int = nil
*p = 10
}
コンパイルは通ったので、実行結果。
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x7cde8]
goroutine 1 [running]:
main.main()
/mnt/nil_deref.go:5 +0x8
runtimeエラーで終了しました。不正なメモリアクセスもしくはnullポインタの記載が出ています。
不正な参照
スコープ外に変数を渡すようにしてみます。
Go
package main
import (
"fmt"
"runtime"
"unsafe"
)
func leakedStack() uintptr {
x := 42
return uintptr(unsafe.Pointer(&x))
}
func leakedHeap() uintptr {
s := make([]int, 1)
s[0] = 123
return uintptr(unsafe.Pointer(&s[0]))
}
func main() {
u1 := leakedStack()
u2 := leakedHeap()
runtime.GC()
p1 := (*int)(unsafe.Pointer(u1))
p2 := (*int)(unsafe.Pointer(u2))
// 値が出たりゴミだったり、環境によりクラッシュもあり得る
fmt.Println("dangling(stack):", *p1)
fmt.Println("dangling(heap) :", *p2)
}
コンパイルは通ったので、実行結果。
dangling(stack): 42
dangling(heap) : 123
設定した値が出ているように見えますが、ゴミ値が出力されている可能性も否めません。
バッファオーバーラン
Go
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
fmt.Println(s[10])
}
コンパイルは通ったので、実行結果。
panic: runtime error: index out of range [10] with length 3
goroutine 1 [running]:
main.main()
/mnt/overrun_panic.go:7 +0x24
配列の定義に対して大きい場所を参照しようとしてruntimeエラーになりました。
まとめ
| 項目 | C言語 | Rust | Go |
|---|---|---|---|
| 解放済みのメモリへのアクセス | コンパイル可能。実行時にクラッシュまたは未定義動作 | コンパイル時に検知 | fatalエラーで終了 |
| Nullポインタ参照 | コンパイル可能。実行時にSegmentation fault | Warningで警告 | 実行時に panic |
| 不正な参照 | コンパイル時に検知 | コンパイル時に検知 | 値が保持されている可能性、もしくはゴミ値を出力 |
| バッファオーバーラン | コンパイル可能。実行時に検知 | コンパイル時に検知 | 実行時に panic |
まだまだGoは勉強中ですが、Rustと違いコンパイル時に問題のありそうな記述は許容され、実行時に不正な挙動にならないように検知しているように伺えました。
今後はコンパイルの観点で色々と観測していきたいと思います。
Discussion