[インデックス 18666] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における32ビットコンパイラ環境でのchecknil
(nilポインタチェック)の誤った挙動を修正するものです。具体的には、ポインタ型ではない整数型に対して不適切にnilチェックが適用され、コンパイル時に問題を引き起こす可能性があったバグを修正しています。この修正により、コンパイラの堅牢性が向上し、特に32ビット環境でのGoプログラムの安定したコンパイルが保証されます。
コミット
commit 3081261b588b1c93da8bf4292e99a535bdb86a3f
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date: Wed Feb 26 12:25:13 2014 -0800
cmd/gc: fix bad checknil with ints on 32 bit compilers
Fixes #7413.
LGTM=rsc
R=remyoudompheng
CC=golang-codereviews, r, rsc
https://golang.org/cl/69180044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3081261b588b1c93da8bf4292e99a535bdb86a3f
元コミット内容
commit 3081261b588b1c93da8bf4292e99a535bdb86a3f
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date: Wed Feb 26 12:25:13 2014 -0800
cmd/gc: fix bad checknil with ints on 32 bit compilers
Fixes #7413.
LGTM=rsc
R=remyoudompheng
CC=golang-codereviews, r, rsc
https://golang.org/cl/69180044
---\n src/cmd/gc/pgen.c | 4 ++--
test/nilptr4.go | 24 ++++++++++++++++++++++++\n 2 files changed, 26 insertions(+), 2 deletions(-)\n\ndiff --git a/src/cmd/gc/pgen.c b/src/cmd/gc/pgen.c
index 1048a62cc8..d05471ee30 100644
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -471,8 +471,8 @@ cgen_checknil(Node *n)\n \n \tif(disable_checknil)\n \t\treturn;\n-\t// Ideally we wouldn\'t see any TUINTPTR here, but we do.\n-\tif(n->type == T || (!isptr[n->type->etype] && n->type->etype != TUINTPTR && n->type->etype != TUNSAFEPTR)) {\n+\t// Ideally we wouldn\'t see any integer types here, but we do.\n+\tif(n->type == T || (!isptr[n->type->etype] && !isint[n->type->etype] && n->type->etype != TUNSAFEPTR)) {\n \t\tdump(\"checknil\", n);\n \t\tfatal(\"bad checknil\");\n \t}\ndiff --git a/test/nilptr4.go b/test/nilptr4.go
new file mode 100644
index 0000000000..3dd7d4e026\n--- /dev/null
+++ b/test/nilptr4.go
@@ -0,0 +1,24 @@
+// build
+\n+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+\n+// Test that the compiler does not crash during compilation.
+\n+package main
+\n+import \"unsafe\"\n+\n+// Issue 7413
+func f1() {\n+\ttype t struct {\n+\t\ti int\n+\t}\n+\n+\tvar v *t\n+\t_ = int(uintptr(unsafe.Pointer(&v.i)))\n+\t_ = int32(uintptr(unsafe.Pointer(&v.i)))\n+}\n+\n+func main() {}\n```
## 変更の背景
このコミットは、Goコンパイラが32ビット環境で特定のコードパターンを処理する際に発生するバグ、具体的にはIssue 7413を修正するために行われました。この問題は、`checknil`というnilポインタチェックを行うコンパイラ内部の関数が、ポインタ型ではない整数型に対して誤ってnilチェックを適用しようとすることで発生していました。
Go言語では、nilポインタのデリファレンス(nilポインタが指すメモリにアクセスしようとすること)はランタイムパニックを引き起こし、プログラムのクラッシュにつながります。これを防ぐため、Goコンパイラはコンパイル時にnilポインタチェックを挿入します。しかし、このチェックがポインタではない型、特に整数型に対して誤って適用されると、コンパイラ自身が不正な状態に陥り、「bad checknil」という致命的なエラーでコンパイルが失敗する可能性がありました。
特に32ビットシステムでは、ポインタのサイズが32ビット(4バイト)であり、メモリのアドレス空間が64ビットシステムよりも限られています。この特性が、`checknil`のロジックと整数型の扱いにおいて、特定のコーナーケースで問題を引き起こす原因となっていたと考えられます。このバグは、コンパイラの安定性と信頼性に直接影響を与えるため、修正が急務でした。
## 前提知識の解説
### Goにおけるnilポインタとnilポインタチェック (`checknil`)
Go言語では、ポインタがどの有効なメモリ位置も指していない状態を`nil`と表現します。`nil`ポインタをデリファレンスしようとすると、Goランタイムは`panic`を発生させ、プログラムを異常終了させます。これはメモリ安全性を保証するための重要なメカニズムです。
Goコンパイラは、このような`nil`ポインタデリファレンスをコンパイル時に検出するために、`checknil`という内部関数を通じてコード中にnilチェックを挿入します。例えば、`p.field`のようなポインタ`p`のフィールドにアクセスする際、`p`が`nil`でないことを確認するコードが自動的に追加されます。これにより、実行時に`nil`ポインタデリファレンスが発生する前に、安全に`panic`を発生させることができます。
### ポインタ型と整数型
* **ポインタ型**: メモリ上の特定のアドレスを指し示すデータ型です。Goでは`*T`のように表現され、`T`型の値が格納されているメモリのアドレスを保持します。
* **整数型**: 数値を格納するデータ型です(例: `int`, `int32`, `uintptr`など)。これらはメモリ上のアドレスを直接指し示すものではありません。
`uintptr`は特殊な整数型で、ポインタを整数として表現するために使用されます。`unsafe.Pointer`と組み合わせて、ポインタと`uintptr`の間で変換を行うことで、低レベルのメモリ操作が可能になります。しかし、`uintptr`自体はポインタではなく、単なる数値です。
### 32ビットコンパイラと64ビットコンパイラ
* **32ビットコンパイラ**: 32ビットアーキテクチャ(例: x86)向けのコードを生成します。この環境では、ポインタのサイズは32ビット(4バイト)です。メモリのアドレス空間は2^32バイト(約4GB)に制限されます。
* **64ビットコンパイラ**: 64ビットアーキテクチャ(例: x86-64)向けのコードを生成します。この環境では、ポインタのサイズは64ビット(8バイト)です。メモリのアドレス空間は2^64バイトと非常に広大です。
ポインタのサイズが異なるため、コンパイラがメモリを扱う方法や、ポインタと整数型を区別する方法に違いが生じることがあります。特に、ポインタと整数型の間で不適切な型変換が行われたり、コンパイラが型のセマンティクスを誤解したりすると、32ビット環境で特有の問題が発生する可能性があります。
## 技術的詳細
このバグは、`src/cmd/gc/pgen.c`ファイル内の`cgen_checknil`関数に存在していました。この関数は、Goコンパイラのバックエンドの一部であり、コード生成フェーズでnilポインタチェックを挿入する役割を担っています。
問題の箇所は、`cgen_checknil`関数がnilチェックを行うべきではない型(特に整数型)に対して、誤ってチェックを試みていた点にあります。元のコードでは、nilチェックの対象を決定する条件式が以下のようになっていました。
```c
if(n->type == T || (!isptr[n->type->etype] && n->type->etype != TUINTPTR && n->type->etype != TUNSAFEPTR)) {
この条件式は、「ノードn
の型がT
であるか、またはその型がポインタ型ではなく、かつTUINTPTR
(uintptr
)でもTUNSAFEPTR
(unsafe.Pointer
)でもない場合」にfatal("bad checknil")
エラーを発生させる、というものでした。
しかし、このロジックには抜け穴がありました。TUINTPTR
とTUNSAFEPTR
は明示的に除外されていましたが、それ以外の通常の整数型(例: int
, int32
など)はisptr
がfalse
であるため、この条件に合致してしまい、fatal("bad checknil")
が呼び出される可能性があったのです。
特に32ビットコンパイラ環境では、ポインタと整数型の内部表現や、それらの間の変換に関する微妙な違いが、この誤ったnilチェックのトリガーとなっていたと考えられます。例えば、構造体のフィールドへのポインタをuintptr
に変換し、さらにそれを整数型にキャストするような操作(test/nilptr4.go
のテストケースに見られるようなパターン)が、コンパイラ内部でchecknil
の対象として誤認識されることがあったようです。
この修正は、!isint[n->type->etype]
という条件を追加することで、この問題を解決しました。これにより、ノードの型がポインタ型ではないかつ整数型でもない場合にのみfatal
エラーを発生させるようになり、通常の整数型が誤ってnilチェックの対象となることを防ぎます。
コアとなるコードの変更箇所
src/cmd/gc/pgen.c
ファイルのcgen_checknil
関数内の条件式が変更されました。
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -471,8 +471,8 @@ cgen_checknil(Node *n)\
if(disable_checknil)
return;
- // Ideally we wouldn't see any TUINTPTR here, but we do.
- if(n->type == T || (!isptr[n->type->etype] && n->type->etype != TUINTPTR && n->type->etype != TUNSAFEPTR)) {
+ // Ideally we wouldn't see any integer types here, but we do.
+ if(n->type == T || (!isptr[n->type->etype] && !isint[n->type->etype] && n->type->etype != TUNSAFEPTR)) {
dump("checknil", n);
fatal("bad checknil");
}
また、この修正の動作を確認するための新しいテストケースtest/nilptr4.go
が追加されました。
// build
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Test that the compiler does not crash during compilation.
package main
import "unsafe"
// Issue 7413
func f1() {
type t struct {
i int
}
var v *t
_ = int(uintptr(unsafe.Pointer(&v.i)))
_ = int32(uintptr(unsafe.Pointer(&v.i)))
}
func main() {}
コアとなるコードの解説
変更された行は、cgen_checknil
関数内のif
文の条件式です。
変更前:
if(n->type == T || (!isptr[n->type->etype] && n->type->etype != TUINTPTR && n->type->etype != TUNSAFEPTR)) {
この条件は、n
の型がポインタ型ではない場合に、それがTUINTPTR
(uintptr
)やTUNSAFEPTR
(unsafe.Pointer
)でない限り、fatal("bad checknil")
エラーを発生させるというものでした。しかし、int
やint32
のような通常の整数型はisptr
がfalse
であり、かつTUINTPTR
やTUNSAFEPTR
でもないため、この条件に合致してしまい、コンパイラが誤ってnilチェックを適用しようとしていました。
変更後:
if(n->type == T || (!isptr[n->type->etype] && !isint[n->type->etype] && n->type->etype != TUNSAFEPTR)) {
この変更では、!isint[n->type->etype]
という新しい条件が追加されました。これは、「ノードn
の型が整数型ではない」ことを意味します。
これにより、条件式は以下のようになります。
「ノードn
の型がT
であるか、または(その型がポインタ型ではなく かつ 整数型でもなく かつ TUNSAFEPTR
でもない)場合」にfatal("bad checknil")
エラーを発生させる。
この修正により、int
やint32
のような純粋な整数型は、isint[n->type->etype]
がtrue
となるため、!isint[n->type->etype]
の条件がfalse
となり、if
文の全体がfalse
になります。結果として、これらの整数型に対してはfatal("bad checknil")
が呼び出されなくなり、コンパイラが正しく動作するようになります。
TUINTPTR
が条件から削除されたのは、isint
配列がuintptr
を含むすべての整数型を適切に識別するようになったため、TUINTPTR
を個別に除外する必要がなくなったためと考えられます。
追加されたtest/nilptr4.go
は、この修正が正しく機能することを確認するためのものです。このテストは、構造体のフィールドへのポインタをunsafe.Pointer
経由でuintptr
に変換し、さらにそれをint
やint32
にキャストするコードを含んでいます。修正前のコンパイラでは、このようなコードが「bad checknil」エラーを引き起こす可能性がありましたが、修正後は問題なくコンパイルが成功するはずです。
関連リンク
- Go Issue 7413: https://github.com/golang/go/issues/7413
- Go Change-Id:
I3081261b588b1c93da8bf4292e99a535bdb86a3f
(Gerrit Change-Id) - Go CL 69180044: https://golang.org/cl/69180044 (このコミットに対応するGoのコードレビューシステムGerritのチェンジリスト)
参考にした情報源リンク
- Go compiler nil pointer checks: https://ardanlabs.com/blog/2019/07/go-nil-pointer-checks.html
- Go 32-bit compilation: https://stackoverflow.com/questions/tagged/go+32-bit
uintptr
andunsafe.Pointer
in Go: https://pkg.go.dev/unsafe- Go issue tracker: https://github.com/golang/go/issues