🧰

Goに入門するので何をするとクラッシュをするか実験をしたい

に公開

はじめに

Goを書く機会が出てきたので入門します。
前回はRustにチャレンジしており、興味があれば参照ください。
https://zenn.dev/ka_kan/articles/2664adeb06a89b

マシンスペック

  • 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